From b714ab18a548ff38fea565fbdb35a9ef24eb80e7 Mon Sep 17 00:00:00 2001 From: punchready Date: Tue, 4 Apr 2023 05:08:17 +0200 Subject: [PATCH 01/27] Completely rewrite STR handling --- TShockAPI/Bouncer.cs | 4 +- .../Handlers/SendTileRectHandlerRefactor.cs | 841 ++++++++++++++++++ 2 files changed, 843 insertions(+), 2 deletions(-) create mode 100644 TShockAPI/Handlers/SendTileRectHandlerRefactor.cs diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs index 6ff7fd1aa..c7c72f968 100644 --- a/TShockAPI/Bouncer.cs +++ b/TShockAPI/Bouncer.cs @@ -35,7 +35,7 @@ namespace TShockAPI /// Bouncer is the TShock anti-hack and anti-cheat system. internal sealed class Bouncer { - internal Handlers.SendTileRectHandler STSHandler { get; private set; } + internal Handlers.SendTileRectHandlerRefactor STSHandler { get; private set; } internal Handlers.NetModules.NetModulePacketHandler NetModuleHandler { get; private set; } internal Handlers.EmojiHandler EmojiHandler { get; private set; } internal Handlers.IllegalPerSe.EmojiPlayerMismatch EmojiPlayerMismatch { get; private set; } @@ -83,7 +83,7 @@ internal class BuffLimit /// A new Bouncer. internal Bouncer() { - STSHandler = new Handlers.SendTileRectHandler(); + STSHandler = new Handlers.SendTileRectHandlerRefactor(); GetDataHandlers.SendTileRect += STSHandler.OnReceive; NetModuleHandler = new Handlers.NetModules.NetModulePacketHandler(); diff --git a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs new file mode 100644 index 000000000..86048193c --- /dev/null +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -0,0 +1,841 @@ +using System.Collections.Generic; +using System.IO; + +using Terraria; +using Terraria.ID; + +using TShockAPI.Net; + +namespace TShockAPI.Handlers +{ + /// + /// Provides processors for handling tile rect packets. + /// This required many hours of reverse engineering work, and is kindly provided to TShock for free by @punchready. + /// + public sealed class SendTileRectHandlerRefactor : IPacketHandler + { + /// + /// Represents a tile rectangle sent through the packet. + /// + private sealed class TileRect + { + private readonly NetTile[,] _tiles; + public readonly int X; + public readonly int Y; + public readonly int Width; + public readonly int Height; + + /// + /// Accesses the tiles contained in this rect. + /// + /// The X coordinate within the rect. + /// The Y coordinate within the rect. + /// The tile at the given position within the rect. + public NetTile this[int x, int y] => _tiles[x, y]; + + /// + /// Constructs a new tile rect based on the given information. + /// + public TileRect(NetTile[,] tiles, int x, int y, int width, int height) + { + _tiles = tiles; + X = x; + Y = y; + Width = width; + Height = height; + } + + /// + /// Reads a tile rect from the given stream. + /// + /// The resulting tile rect. + public static TileRect Read(MemoryStream stream, int tileX, int tileY, int width, int height) + { + NetTile[,] tiles = new NetTile[width, height]; + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + tiles[x, y] = new NetTile(); + tiles[x, y].Unpack(stream); // explicit > implicit + } + } + return new TileRect(tiles, tileX, tileY, width, height); + } + } + + /// + /// Represents a common tile rect operation (Placement, State Change, Removal). + /// + private readonly struct TileRectMatch + { + public const short IGNORE_FRAME = -1; + + private enum MatchType + { + Placement, + StateChange, + Removal, + } + + private readonly int Width; + private readonly int Height; + + private readonly ushort TileType; + private readonly short MaxFrameX; + private readonly short MaxFrameY; + + private readonly MatchType Type; + + private TileRectMatch(MatchType type, int width, int height, ushort tileType, short maxFrameX, short maxFrameY) + { + Type = type; + Width = width; + Height = height; + TileType = tileType; + MaxFrameX = maxFrameX; + MaxFrameY = maxFrameY; + } + + /// + /// Creates a new placement operation. + /// + /// The width of the placement. + /// The height of the placement. + /// The tile type of the placement. + /// The maximum allowed frameX of the placement, or if this operation does not change frameX. + /// The maximum allowed frameY of the placement, or if this operation does not change frameY. + /// The resulting operation match. + public static TileRectMatch Placement(int width, int height, ushort tileType, short maxFrameX, short maxFrameY) + { + return new TileRectMatch(MatchType.Placement, width, height, tileType, maxFrameX, maxFrameY); + } + + /// + /// Creates a new state change operation. + /// + /// The width of the state change. + /// The height of the state change. + /// The target tile type of the state change. + /// The maximum allowed frameX of the state change, or if this operation does not change frameX. + /// The maximum allowed frameY of the state change, or if this operation does not change frameY. + /// The resulting operation match. + public static TileRectMatch StateChange(int width, int height, ushort tileType, short maxFrameX, short maxFrameY) + { + return new TileRectMatch(MatchType.StateChange, width, height, tileType, maxFrameX, maxFrameY); + } + + /// + /// Creates a new removal operation. + /// + /// The width of the removal. + /// The height of the removal. + /// The target tile type of the removal. + /// The resulting operation match. + public static TileRectMatch Removal(int width, int height, ushort tileType) + { + return new TileRectMatch(MatchType.Removal, width, height, tileType, 0, 0); + } + + /// + /// Determines whether the given tile rectangle matches this operation, and if so, applies it to the world. + /// + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches this operation and the changes have been applied, otherwise . + public bool Matches(TSPlayer player, TileRect rect) + { + if (rect.Width != Width || rect.Height != Height) + { + return false; + } + + for (int x = 0; x < rect.Width; x++) + { + for (int y = 0; y < rect.Height; y++) + { + NetTile tile = rect[x, y]; + if (Type is MatchType.Placement or MatchType.StateChange) + { + if (tile.Type != TileType) + { + return false; + } + } + if (Type is MatchType.Placement or MatchType.StateChange) + { + if (MaxFrameX != IGNORE_FRAME) + { + if (tile.FrameX < 0 || tile.FrameX > MaxFrameX) + { + return false; + } + } + if (MaxFrameY != IGNORE_FRAME) + { + if (tile.FrameY < 0 || tile.FrameY > MaxFrameY) + { + return false; + } + } + } + if (Type == MatchType.Removal) + { + if (tile.Active) + { + return false; + } + } + } + } + + for (int x = rect.X; x < rect.X + rect.Width; x++) + { + for (int y = rect.Y; y < rect.Y + rect.Height; y++) + { + if (!player.HasBuildPermission(x, y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + } + } + + switch (Type) + { + case MatchType.Placement: + { + return MatchPlacement(player, rect); + } + case MatchType.StateChange: + { + return MatchStateChange(player, rect); + } + case MatchType.Removal: + { + return MatchRemoval(player, rect); + } + } + + return false; + } + + private bool MatchPlacement(TSPlayer player, TileRect rect) + { + for (int x = rect.X; x < rect.Y + rect.Width; x++) + { + for (int y = rect.Y; y < rect.Y + rect.Height; y++) + { + if (Main.tile[x, y].active() && !(Main.tile[x, y].type != TileID.RollingCactus && (Main.tileCut[Main.tile[x, y].type] || TileID.Sets.BreakableWhenPlacing[Main.tile[x, y].type]))) + { + return false; + } + } + } + + // let's hope tile types never go out of short range (they use ushort in terraria's code) + if (TShock.TileBans.TileIsBanned((short)TileType, player)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + for (int x = 0; x < rect.Width; x++) + { + for (int y = 0; y < rect.Height; y++) + { + Main.tile[x + rect.X, y + rect.Y].active(active: true); + Main.tile[x + rect.X, y + rect.Y].type = rect[x, y].Type; + if (MaxFrameX != IGNORE_FRAME) + { + Main.tile[x + rect.X, y + rect.Y].frameX = rect[x, y].FrameX; + } + if (MaxFrameY != IGNORE_FRAME) + { + Main.tile[x + rect.X, y + rect.Y].frameY = rect[x, y].FrameY; + } + } + } + + return true; + } + + private bool MatchStateChange(TSPlayer player, TileRect rect) + { + for (int x = rect.X; x < rect.Y + rect.Width; x++) + { + for (int y = rect.Y; y < rect.Y + rect.Height; y++) + { + if (!Main.tile[x, y].active() || Main.tile[x, y].type != TileType) + { + return false; + } + } + } + + for (int x = 0; x < rect.Width; x++) + { + for (int y = 0; y < rect.Height; y++) + { + if (MaxFrameX != IGNORE_FRAME) + { + Main.tile[x + rect.X, y + rect.Y].frameX = rect[x, y].FrameX; + } + if (MaxFrameY != IGNORE_FRAME) + { + Main.tile[x + rect.X, y + rect.Y].frameY = rect[x, y].FrameY; + } + } + } + + return true; + } + + private bool MatchRemoval(TSPlayer player, TileRect rect) + { + for (int x = rect.X; x < rect.Y + rect.Width; x++) + { + for (int y = rect.Y; y < rect.Y + rect.Height; y++) + { + if (!Main.tile[x, y].active() || Main.tile[x, y].type != TileType) + { + return false; + } + } + } + + for (int x = 0; x < rect.Width; x++) + { + for (int y = 0; y < rect.Height; y++) + { + Main.tile[x + rect.X, y + rect.Y].active(active: false); + Main.tile[x + rect.X, y + rect.Y].frameX = -1; + Main.tile[x + rect.X, y + rect.Y].frameY = -1; + } + } + + return true; + } + } + + /// + /// Contains the complete list of valid tile rect operations the game currently performs. + /// + private static readonly TileRectMatch[] Matches = new TileRectMatch[] + { + TileRectMatch.Placement(2, 3, TileID.TargetDummy, 54, 36), + TileRectMatch.Placement(3, 4, TileID.TeleportationPylon, 468, 54), + TileRectMatch.Placement(2, 3, TileID.DisplayDoll, 126, 36), + TileRectMatch.Placement(2, 3, TileID.HatRack, 90, 54), + TileRectMatch.Placement(2, 2, TileID.ItemFrame, 162, 18), + TileRectMatch.Placement(3, 3, TileID.WeaponsRack2, 90, 36), + TileRectMatch.Placement(1, 1, TileID.FoodPlatter, 18, 0), + TileRectMatch.Placement(1, 1, TileID.LogicSensor, 108, 0), + + TileRectMatch.StateChange(3, 2, TileID.Campfire, TileRectMatch.IGNORE_FRAME, 54), + TileRectMatch.StateChange(4, 3, TileID.Cannon, TileRectMatch.IGNORE_FRAME, 468), + TileRectMatch.StateChange(2, 2, TileID.ArrowSign, TileRectMatch.IGNORE_FRAME, 270), + TileRectMatch.StateChange(2, 2, TileID.PaintedArrowSign, TileRectMatch.IGNORE_FRAME, 270), + TileRectMatch.StateChange(2, 2, TileID.MusicBoxes, 54, TileRectMatch.IGNORE_FRAME), + TileRectMatch.StateChange(2, 3, TileID.LunarMonolith, TileRectMatch.IGNORE_FRAME, 92), + TileRectMatch.StateChange(2, 3, TileID.BloodMoonMonolith, TileRectMatch.IGNORE_FRAME, 90), + TileRectMatch.StateChange(2, 3, TileID.VoidMonolith, TileRectMatch.IGNORE_FRAME, 90), + TileRectMatch.StateChange(2, 3, TileID.EchoMonolith, TileRectMatch.IGNORE_FRAME, 90), + TileRectMatch.StateChange(2, 3, TileID.ShimmerMonolith, TileRectMatch.IGNORE_FRAME, 144), + TileRectMatch.StateChange(2, 4, TileID.WaterFountain, TileRectMatch.IGNORE_FRAME, 126), + TileRectMatch.StateChange(1, 1, TileID.Candles, 18, TileRectMatch.IGNORE_FRAME), + TileRectMatch.StateChange(1, 1, TileID.PeaceCandle, 18, TileRectMatch.IGNORE_FRAME), + TileRectMatch.StateChange(1, 1, TileID.WaterCandle, 18, TileRectMatch.IGNORE_FRAME), + TileRectMatch.StateChange(1, 1, TileID.PlatinumCandle, 18, TileRectMatch.IGNORE_FRAME), + TileRectMatch.StateChange(1, 1, TileID.ShadowCandle, 18, TileRectMatch.IGNORE_FRAME), + TileRectMatch.StateChange(1, 1, TileID.Traps, 90, 90), + TileRectMatch.StateChange(1, 1, TileID.WirePipe, 36, TileRectMatch.IGNORE_FRAME), + TileRectMatch.StateChange(1, 1, TileID.ProjectilePressurePad, 66, TileRectMatch.IGNORE_FRAME), + TileRectMatch.StateChange(1, 1, TileID.Plants, 792, TileRectMatch.IGNORE_FRAME), + TileRectMatch.StateChange(1, 1, TileID.MinecartTrack, 36, TileRectMatch.IGNORE_FRAME), + + TileRectMatch.Removal(1, 2, TileID.Firework), + TileRectMatch.Removal(1, 1, TileID.LandMine), + }; + + + /// + /// Handles a packet receive event. + /// + public void OnReceive(object sender, GetDataHandlers.SendTileRectEventArgs args) + { + // this permission bypasses all checks for direct access to the world + if (args.Player.HasPermission(Permissions.allowclientsideworldedit)) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect accepted clientside world edit from {args.Player.Name}")); + + // use vanilla handling + args.Handled = false; + return; + } + + // this handler handles the entire logic of this packet + args.Handled = true; + + // player throttled? + if (args.Player.IsBouncerThrottled()) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from throttle from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } + + // player disabled? + if (args.Player.IsBeingDisabled()) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from being disabled from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } + + // as of 1.4 this is the biggest size the client will send in any case, determined by full code analysis + // see default matches above and special cases below + if (args.Width > 4 || args.Length > 4) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from non-vanilla tilemod from {args.Player.Name}")); + + // definitely invalid; do not send any correcting data + return; + } + + // read the tile rectangle + TileRect rect = TileRect.Read(args.Data, args.TileX, args.TileY, args.Width, args.Length); + + // check if the positioning is valid + if (!IsRectPositionValid(args.Player, rect)) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from out of bounds / build permission from {args.Player.Name}")); + + // send nothing due to out of bounds + return; + } + + // a very special case, due to the clentaminator having a larger range than TSPlayer.IsInRange() allows + if (MatchesConversionSpread(args.Player, rect)) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } + + // check if the distance is valid + if (!IsRectDistanceValid(args.Player, rect)) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from out of range from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } + + // a very special case, due to the flower seed check otherwise hijacking this + if (MatchesFlowerBoots(args.Player, rect)) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } + + // check if the rect matches any valid operation + foreach (TileRectMatch match in Matches) + { + if (match.Matches(args.Player, rect)) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } + } + + // a few special cases + if ( + MatchesConversionSpread(args.Player, rect) || + MatchesGrassMow(args.Player, rect) || + MatchesChristmasTree(args.Player, rect) + ) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } + + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from matches from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } + + /// + /// Checks whether the tile rect is at a valid position for the given player. + /// + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect at a valid position, otherwise . + private static bool IsRectPositionValid(TSPlayer player, TileRect rect) + { + for (int x = 0; x < rect.Width; x++) + { + for (int y = 0; y < rect.Height; y++) + { + int realX = rect.X + x; + int realY = rect.Y + y; + + if (realX < 0 || realX >= Main.maxTilesX || realY < 0 || realY >= Main.maxTilesY) + { + return false; + } + } + } + + return true; + } + + /// + /// Checks whether the tile rect is at a valid distance to the given player. + /// + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect at a valid distance, otherwise . + private static bool IsRectDistanceValid(TSPlayer player, TileRect rect) + { + for (int x = 0; x < rect.Width; x++) + { + for (int y = 0; y < rect.Height; y++) + { + int realX = rect.X + x; + int realY = rect.Y + y; + + if (!player.IsInRange(realX, realY)) + { + return false; + } + } + } + + return true; + } + + + /// + /// Checks whether the tile rect is a valid conversion spread (Clentaminator, Powders, etc.) + /// + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches a conversion spread operation, otherwise . + private static bool MatchesConversionSpread(TSPlayer player, TileRect rect) + { + if (rect.Width != 1 || rect.Height != 1) + { + return false; + } + + ITile oldTile = Main.tile[rect.X, rect.Y]; + NetTile newTile = rect[0, 0]; + + bool matchedTileOrWall = false; + + if (oldTile.active()) + { + if ( + ( + (TileID.Sets.Conversion.Stone[oldTile.type] || Main.tileMoss[oldTile.type]) && + (TileID.Sets.Conversion.Stone[newTile.Type] || Main.tileMoss[newTile.Type]) + ) || + ( + (oldTile.type == TileID.Dirt || oldTile.type == TileID.Mud) && + (newTile.Type == TileID.Dirt || newTile.Type == TileID.Mud) + ) || + TileID.Sets.Conversion.Grass[oldTile.type] && TileID.Sets.Conversion.Grass[newTile.Type] || + TileID.Sets.Conversion.Ice[oldTile.type] && TileID.Sets.Conversion.Ice[newTile.Type] || + TileID.Sets.Conversion.Sand[oldTile.type] && TileID.Sets.Conversion.Sand[newTile.Type] || + TileID.Sets.Conversion.Sandstone[oldTile.type] && TileID.Sets.Conversion.Sandstone[newTile.Type] || + TileID.Sets.Conversion.HardenedSand[oldTile.type] && TileID.Sets.Conversion.HardenedSand[newTile.Type] || + TileID.Sets.Conversion.Thorn[oldTile.type] && TileID.Sets.Conversion.Thorn[newTile.Type] || + TileID.Sets.Conversion.Moss[oldTile.type] && TileID.Sets.Conversion.Moss[newTile.Type] || + TileID.Sets.Conversion.MossBrick[oldTile.type] && TileID.Sets.Conversion.MossBrick[newTile.Type] + ) + { + if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + matchedTileOrWall = true; + } + else if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + matchedTileOrWall = true; + } + else + { + Main.tile[rect.X, rect.Y].type = newTile.Type; + Main.tile[rect.X, rect.Y].frameX = newTile.FrameX; + Main.tile[rect.X, rect.Y].frameY = newTile.FrameY; + + matchedTileOrWall = true; + } + } + } + + if (oldTile.wall != 0) + { + if ( + WallID.Sets.Conversion.Stone[oldTile.wall] && WallID.Sets.Conversion.Stone[newTile.Wall] || + WallID.Sets.Conversion.Grass[oldTile.wall] && WallID.Sets.Conversion.Grass[newTile.Wall] || + WallID.Sets.Conversion.Sandstone[oldTile.wall] && WallID.Sets.Conversion.Sandstone[newTile.Wall] || + WallID.Sets.Conversion.HardenedSand[oldTile.wall] && WallID.Sets.Conversion.HardenedSand[newTile.Wall] || + WallID.Sets.Conversion.PureSand[oldTile.wall] && WallID.Sets.Conversion.PureSand[newTile.Wall] || + WallID.Sets.Conversion.NewWall1[oldTile.wall] && WallID.Sets.Conversion.NewWall1[newTile.Wall] || + WallID.Sets.Conversion.NewWall2[oldTile.wall] && WallID.Sets.Conversion.NewWall2[newTile.Wall] || + WallID.Sets.Conversion.NewWall3[oldTile.wall] && WallID.Sets.Conversion.NewWall3[newTile.Wall] || + WallID.Sets.Conversion.NewWall4[oldTile.wall] && WallID.Sets.Conversion.NewWall4[newTile.Wall] + ) + { + // wallbans when? + + if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + matchedTileOrWall = true; + } + else + { + Main.tile[rect.X, rect.Y].wall = newTile.Wall; + + matchedTileOrWall = true; + } + } + } + + return matchedTileOrWall; + } + + + private static readonly Dictionary> PlantToGrassMap = new Dictionary> + { + { TileID.Plants, new HashSet() + { + TileID.Grass, TileID.GolfGrass + } }, + { TileID.HallowedPlants, new HashSet() + { + TileID.HallowedGrass, TileID.GolfGrassHallowed + } }, + { TileID.HallowedPlants2, new HashSet() + { + TileID.HallowedGrass, TileID.GolfGrassHallowed + } }, + { TileID.JunglePlants2, new HashSet() + { + TileID.JungleGrass + } }, + { TileID.AshPlants, new HashSet() + { + TileID.AshGrass + } }, + }; + + private static readonly Dictionary> GrassToStyleMap = new Dictionary>() + { + { TileID.Plants, new HashSet() + { + 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 24, 27, 30, 33, 36, 39, 42, + 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, 44, + } }, + { TileID.HallowedPlants, new HashSet() + { + 4, 6, + } }, + { TileID.HallowedPlants2, new HashSet() + { + 2, 3, 4, 6, 7, + } }, + { TileID.JunglePlants2, new HashSet() + { + 9, 10, 11, 12, 13, 14, 15, 16, + } }, + { TileID.AshPlants, new HashSet() + { + 6, 7, 8, 9, 10, + } }, + }; + + /// + /// Checks whether the tile rect is a valid Flower Boots placement. + /// + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches a Flower Boots placement, otherwise . + private static bool MatchesFlowerBoots(TSPlayer player, TileRect rect) + { + if (rect.Width != 1 || rect.Height != 1) + { + return false; + } + + if (!player.TPlayer.flowerBoots) + { + return false; + } + + ITile oldTile = Main.tile[rect.X, rect.Y]; + NetTile newTile = rect[0, 0]; + + if ( + PlantToGrassMap.TryGetValue(newTile.Type, out HashSet grassTiles) && + !oldTile.active() && grassTiles.Contains(Main.tile[rect.X, rect.Y + 1].type) && + GrassToStyleMap[newTile.Type].Contains((ushort)(newTile.FrameX / 18)) + ) + { + if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + Main.tile[rect.X, rect.Y].active(active: true); + Main.tile[rect.X, rect.Y].type = newTile.Type; + Main.tile[rect.X, rect.Y].frameX = newTile.FrameX; + Main.tile[rect.X, rect.Y].frameY = 0; + + return true; + } + + return false; + } + + + private static readonly Dictionary GrassToMowedMap = new Dictionary + { + { TileID.Grass, TileID.GolfGrass }, + { TileID.HallowedGrass, TileID.GolfGrassHallowed }, + }; + + /// + /// Checks whether the tile rect is a valid grass mow. + /// + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches a grass mowing operation, otherwise . + private static bool MatchesGrassMow(TSPlayer player, TileRect rect) + { + if (rect.Width != 1 || rect.Height != 1) + { + return false; + } + + ITile oldTile = Main.tile[rect.X, rect.Y]; + NetTile newTile = rect[0, 0]; + + if (GrassToMowedMap.TryGetValue(oldTile.type, out ushort mowed) && newTile.Type == mowed) + { + if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + Main.tile[rect.X, rect.Y].type = newTile.Type; + if (!newTile.FrameImportant) + { + Main.tile[rect.X, rect.Y].frameX = -1; + Main.tile[rect.X, rect.Y].frameY = -1; + } + + // prevent a common crash when the game checks all vines in an unlimited horizontal length + if (TileID.Sets.IsVine[Main.tile[rect.X, rect.Y + 1].type]) + { + WorldGen.KillTile(rect.X, rect.Y + 1); + } + + return true; + } + + return false; + } + + + /// + /// Checks whether the tile rect is a valid christmas tree modification. + /// This also required significant reverse engineering effort. + /// + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches a christmas tree operation, otherwise . + private static bool MatchesChristmasTree(TSPlayer player, TileRect rect) + { + if (rect.Width != 1 || rect.Height != 1) + { + return false; + } + + ITile oldTile = Main.tile[rect.X, rect.Y]; + NetTile newTile = rect[0, 0]; + + if (oldTile.type == TileID.ChristmasTree && newTile.Type == TileID.ChristmasTree) + { + if (newTile.FrameX != 10) + { + return false; + } + + int obj_0 = (newTile.FrameY & 0b0000000000000111); + int obj_1 = (newTile.FrameY & 0b0000000000111000) >> 3; + int obj_2 = (newTile.FrameY & 0b0000001111000000) >> 6; + int obj_3 = (newTile.FrameY & 0b0011110000000000) >> 10; + int obj_x = (newTile.FrameY & 0b1100000000000000) >> 14; + + if (obj_x != 0) + { + return false; + } + + if (obj_0 is < 0 or > 4 || obj_1 is < 0 or > 6 || obj_2 is < 0 or > 11 || obj_3 is < 0 or > 11) + { + return false; + } + + if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + Main.tile[rect.X, rect.Y].frameY = newTile.FrameY; + + return true; + } + + return false; + } + } +} From 2fdf096ce8104a1a77bbc0a7394fddb1634f75ff Mon Sep 17 00:00:00 2001 From: punchready Date: Tue, 4 Apr 2023 05:10:37 +0200 Subject: [PATCH 02/27] Update changelog --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index c4a95d142..48c965d50 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -109,7 +109,7 @@ Use past tense when adding new entries; sign your name off when you add or chang * Relaxed custom death message restrictions to allow Inferno potions in PvP. (@drunderscore) * Allowed Flower Boots to place Ash Flowers on Ash Grass blocks. (@punchready) * Removed unnecessary range check that artifically shortened quick stack reach. (@boddyn, #2885, @bcat) -* Improved the exploit protection in tile rect handling. (@punchready) +* Re-wrote tile rect handling from scratch, fixing a certain exploitable flaw in the old code and significantly reducing the potential exploit surface, potentially even down to zero. (@punchready) ## TShock 5.1.3 * Added support for Terraria 1.4.4.9 via OTAPI 3.1.20. (@SignatureBeef) From 3302b4653ea986bcd6a8d7f813430fb1e6514031 Mon Sep 17 00:00:00 2001 From: punchready Date: Tue, 4 Apr 2023 05:51:53 +0200 Subject: [PATCH 03/27] Update STR checks to be even more strict --- .../Handlers/SendTileRectHandlerRefactor.cs | 127 ++++++++++++------ 1 file changed, 86 insertions(+), 41 deletions(-) diff --git a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs index 86048193c..9b8b0096e 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -69,7 +69,7 @@ public static TileRect Read(MemoryStream stream, int tileX, int tileY, int width /// private readonly struct TileRectMatch { - public const short IGNORE_FRAME = -1; + private const short IGNORE_FRAME = -1; private enum MatchType { @@ -84,10 +84,12 @@ private enum MatchType private readonly ushort TileType; private readonly short MaxFrameX; private readonly short MaxFrameY; + private readonly short FrameXStep; + private readonly short FrameYStep; private readonly MatchType Type; - private TileRectMatch(MatchType type, int width, int height, ushort tileType, short maxFrameX, short maxFrameY) + private TileRectMatch(MatchType type, int width, int height, ushort tileType, short maxFrameX, short maxFrameY, short frameXStep, short frameYStep) { Type = type; Width = width; @@ -95,6 +97,8 @@ private TileRectMatch(MatchType type, int width, int height, ushort tileType, sh TileType = tileType; MaxFrameX = maxFrameX; MaxFrameY = maxFrameY; + FrameXStep = frameXStep; + FrameYStep = frameYStep; } /// @@ -105,10 +109,12 @@ private TileRectMatch(MatchType type, int width, int height, ushort tileType, sh /// The tile type of the placement. /// The maximum allowed frameX of the placement, or if this operation does not change frameX. /// The maximum allowed frameY of the placement, or if this operation does not change frameY. + /// The step size in which frameX changes for this placement, or 1 if any value is allowed. + /// The step size in which frameX changes for this placement, or 1 if any value is allowed. /// The resulting operation match. - public static TileRectMatch Placement(int width, int height, ushort tileType, short maxFrameX, short maxFrameY) + public static TileRectMatch Placement(int width, int height, ushort tileType, short maxFrameX, short maxFrameY, short frameXStep, short frameYStep) { - return new TileRectMatch(MatchType.Placement, width, height, tileType, maxFrameX, maxFrameY); + return new TileRectMatch(MatchType.Placement, width, height, tileType, maxFrameX, maxFrameY, frameXStep, frameYStep); } /// @@ -117,12 +123,42 @@ public static TileRectMatch Placement(int width, int height, ushort tileType, sh /// The width of the state change. /// The height of the state change. /// The target tile type of the state change. - /// The maximum allowed frameX of the state change, or if this operation does not change frameX. - /// The maximum allowed frameY of the state change, or if this operation does not change frameY. + /// The maximum allowed frameX of the state change. + /// The maximum allowed frameY of the state change. + /// The step size in which frameX changes for this placement, or 1 if any value is allowed. + /// The step size in which frameY changes for this placement, or 1 if any value is allowed. /// The resulting operation match. - public static TileRectMatch StateChange(int width, int height, ushort tileType, short maxFrameX, short maxFrameY) + public static TileRectMatch StateChange(int width, int height, ushort tileType, short maxFrameX, short maxFrameY, short frameXStep, short frameYStep) { - return new TileRectMatch(MatchType.StateChange, width, height, tileType, maxFrameX, maxFrameY); + return new TileRectMatch(MatchType.StateChange, width, height, tileType, maxFrameX, maxFrameY, frameXStep, frameYStep); + } + + /// + /// Creates a new state change operation which only changes frameX. + /// + /// The width of the state change. + /// The height of the state change. + /// The target tile type of the state change. + /// The maximum allowed frameX of the state change. + /// The step size in which frameX changes for this placement, or 1 if any value is allowed. + /// The resulting operation match. + public static TileRectMatch StateChangeX(int width, int height, ushort tileType, short maxFrame, short frameStep) + { + return new TileRectMatch(MatchType.StateChange, width, height, tileType, maxFrame, IGNORE_FRAME, frameStep, 0); + } + + /// + /// Creates a new state change operation which only changes frameY. + /// + /// The width of the state change. + /// The height of the state change. + /// The target tile type of the state change. + /// The maximum allowed frameY of the state change. + /// The step size in which frameY changes for this placement, or 1 if any value is allowed. + /// The resulting operation match. + public static TileRectMatch StateChangeY(int width, int height, ushort tileType, short maxFrame, short frameStep) + { + return new TileRectMatch(MatchType.StateChange, width, height, tileType, IGNORE_FRAME, maxFrame, 0, frameStep); } /// @@ -134,7 +170,7 @@ public static TileRectMatch StateChange(int width, int height, ushort tileType, /// The resulting operation match. public static TileRectMatch Removal(int width, int height, ushort tileType) { - return new TileRectMatch(MatchType.Removal, width, height, tileType, 0, 0); + return new TileRectMatch(MatchType.Removal, width, height, tileType, 0, 0, 0, 0); } /// @@ -166,14 +202,14 @@ public bool Matches(TSPlayer player, TileRect rect) { if (MaxFrameX != IGNORE_FRAME) { - if (tile.FrameX < 0 || tile.FrameX > MaxFrameX) + if (tile.FrameX < 0 || tile.FrameX > MaxFrameX || tile.FrameX % FrameXStep != 0) { return false; } } if (MaxFrameY != IGNORE_FRAME) { - if (tile.FrameY < 0 || tile.FrameY > MaxFrameY) + if (tile.FrameY < 0 || tile.FrameY > MaxFrameY || tile.FrameY % FrameYStep != 0) { return false; } @@ -321,38 +357,47 @@ private bool MatchRemoval(TSPlayer player, TileRect rect) /// /// Contains the complete list of valid tile rect operations the game currently performs. /// + // The matches restrict the tile rects to only place one kind of tile, and only with the given maximum values and step sizes for frameX and frameY. This performs pretty much perfect checks on the data, allowing only valid placements. + // For TileID.MinecartTrack, the data is taken from `Minecart._trackSwitchOptions`, allowing any framing value in this array (currently 0-36). + // For TileID.Plants, it is taken from `ItemID.Sets.flowerPacketInfo[n].stylesOnPurity`, allowing every style multiplied by 18. + // The other operations are based on code analysis and manual observation. private static readonly TileRectMatch[] Matches = new TileRectMatch[] { - TileRectMatch.Placement(2, 3, TileID.TargetDummy, 54, 36), - TileRectMatch.Placement(3, 4, TileID.TeleportationPylon, 468, 54), - TileRectMatch.Placement(2, 3, TileID.DisplayDoll, 126, 36), - TileRectMatch.Placement(2, 3, TileID.HatRack, 90, 54), - TileRectMatch.Placement(2, 2, TileID.ItemFrame, 162, 18), - TileRectMatch.Placement(3, 3, TileID.WeaponsRack2, 90, 36), - TileRectMatch.Placement(1, 1, TileID.FoodPlatter, 18, 0), - TileRectMatch.Placement(1, 1, TileID.LogicSensor, 108, 0), - - TileRectMatch.StateChange(3, 2, TileID.Campfire, TileRectMatch.IGNORE_FRAME, 54), - TileRectMatch.StateChange(4, 3, TileID.Cannon, TileRectMatch.IGNORE_FRAME, 468), - TileRectMatch.StateChange(2, 2, TileID.ArrowSign, TileRectMatch.IGNORE_FRAME, 270), - TileRectMatch.StateChange(2, 2, TileID.PaintedArrowSign, TileRectMatch.IGNORE_FRAME, 270), - TileRectMatch.StateChange(2, 2, TileID.MusicBoxes, 54, TileRectMatch.IGNORE_FRAME), - TileRectMatch.StateChange(2, 3, TileID.LunarMonolith, TileRectMatch.IGNORE_FRAME, 92), - TileRectMatch.StateChange(2, 3, TileID.BloodMoonMonolith, TileRectMatch.IGNORE_FRAME, 90), - TileRectMatch.StateChange(2, 3, TileID.VoidMonolith, TileRectMatch.IGNORE_FRAME, 90), - TileRectMatch.StateChange(2, 3, TileID.EchoMonolith, TileRectMatch.IGNORE_FRAME, 90), - TileRectMatch.StateChange(2, 3, TileID.ShimmerMonolith, TileRectMatch.IGNORE_FRAME, 144), - TileRectMatch.StateChange(2, 4, TileID.WaterFountain, TileRectMatch.IGNORE_FRAME, 126), - TileRectMatch.StateChange(1, 1, TileID.Candles, 18, TileRectMatch.IGNORE_FRAME), - TileRectMatch.StateChange(1, 1, TileID.PeaceCandle, 18, TileRectMatch.IGNORE_FRAME), - TileRectMatch.StateChange(1, 1, TileID.WaterCandle, 18, TileRectMatch.IGNORE_FRAME), - TileRectMatch.StateChange(1, 1, TileID.PlatinumCandle, 18, TileRectMatch.IGNORE_FRAME), - TileRectMatch.StateChange(1, 1, TileID.ShadowCandle, 18, TileRectMatch.IGNORE_FRAME), - TileRectMatch.StateChange(1, 1, TileID.Traps, 90, 90), - TileRectMatch.StateChange(1, 1, TileID.WirePipe, 36, TileRectMatch.IGNORE_FRAME), - TileRectMatch.StateChange(1, 1, TileID.ProjectilePressurePad, 66, TileRectMatch.IGNORE_FRAME), - TileRectMatch.StateChange(1, 1, TileID.Plants, 792, TileRectMatch.IGNORE_FRAME), - TileRectMatch.StateChange(1, 1, TileID.MinecartTrack, 36, TileRectMatch.IGNORE_FRAME), + TileRectMatch.Placement(2, 3, TileID.TargetDummy, 54, 36, 18, 18), + TileRectMatch.Placement(3, 4, TileID.TeleportationPylon, 468, 54, 18, 18), + TileRectMatch.Placement(2, 3, TileID.DisplayDoll, 126, 36, 18, 18), + TileRectMatch.Placement(2, 3, TileID.HatRack, 90, 54, 18, 18), + TileRectMatch.Placement(2, 2, TileID.ItemFrame, 162, 18, 18, 18), + TileRectMatch.Placement(3, 3, TileID.WeaponsRack2, 90, 36, 18, 18), + TileRectMatch.Placement(1, 1, TileID.FoodPlatter, 18, 0, 18, 18), + TileRectMatch.Placement(1, 1, TileID.LogicSensor, 108, 0, 18, 18), + + TileRectMatch.StateChangeY(3, 2, TileID.Campfire, 54, 18), + TileRectMatch.StateChangeY(4, 3, TileID.Cannon, 468, 18), + TileRectMatch.StateChangeY(2, 2, TileID.ArrowSign, 270, 18), + TileRectMatch.StateChangeY(2, 2, TileID.PaintedArrowSign, 270, 18), + + TileRectMatch.StateChangeX(2, 2, TileID.MusicBoxes, 54, 18), + + TileRectMatch.StateChangeY(2, 3, TileID.LunarMonolith, 92, 18), + TileRectMatch.StateChangeY(2, 3, TileID.BloodMoonMonolith, 90, 18), + TileRectMatch.StateChangeY(2, 3, TileID.VoidMonolith, 90, 18), + TileRectMatch.StateChangeY(2, 3, TileID.EchoMonolith, 90, 18), + TileRectMatch.StateChangeY(2, 3, TileID.ShimmerMonolith, 144, 18), + TileRectMatch.StateChangeY(2, 4, TileID.WaterFountain, 126, 18), + + TileRectMatch.StateChangeX(1, 1, TileID.Candles, 18, 18), + TileRectMatch.StateChangeX(1, 1, TileID.PeaceCandle, 18, 18), + TileRectMatch.StateChangeX(1, 1, TileID.WaterCandle, 18, 18), + TileRectMatch.StateChangeX(1, 1, TileID.PlatinumCandle, 18, 18), + TileRectMatch.StateChangeX(1, 1, TileID.ShadowCandle, 18, 18), + + TileRectMatch.StateChange(1, 1, TileID.Traps, 90, 90, 18, 18), + + TileRectMatch.StateChangeX(1, 1, TileID.WirePipe, 36, 18), + TileRectMatch.StateChangeX(1, 1, TileID.ProjectilePressurePad, 66, 22), + TileRectMatch.StateChangeX(1, 1, TileID.Plants, 792, 18), + TileRectMatch.StateChangeX(1, 1, TileID.MinecartTrack, 36, 1), TileRectMatch.Removal(1, 2, TileID.Firework), TileRectMatch.Removal(1, 1, TileID.LandMine), From 26482da23f70c8b22fa00b5ccdc853bc4e0e0f68 Mon Sep 17 00:00:00 2001 From: punchready Date: Tue, 4 Apr 2023 06:02:55 +0200 Subject: [PATCH 04/27] Remove frame ignoring from tile rect placement operations --- TShockAPI/Handlers/SendTileRectHandlerRefactor.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs index 9b8b0096e..a3c87100f 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -107,8 +107,8 @@ private TileRectMatch(MatchType type, int width, int height, ushort tileType, sh /// The width of the placement. /// The height of the placement. /// The tile type of the placement. - /// The maximum allowed frameX of the placement, or if this operation does not change frameX. - /// The maximum allowed frameY of the placement, or if this operation does not change frameY. + /// The maximum allowed frameX of the placement. + /// The maximum allowed frameY of the placement. /// The step size in which frameX changes for this placement, or 1 if any value is allowed. /// The step size in which frameX changes for this placement, or 1 if any value is allowed. /// The resulting operation match. @@ -282,14 +282,8 @@ private bool MatchPlacement(TSPlayer player, TileRect rect) { Main.tile[x + rect.X, y + rect.Y].active(active: true); Main.tile[x + rect.X, y + rect.Y].type = rect[x, y].Type; - if (MaxFrameX != IGNORE_FRAME) - { - Main.tile[x + rect.X, y + rect.Y].frameX = rect[x, y].FrameX; - } - if (MaxFrameY != IGNORE_FRAME) - { - Main.tile[x + rect.X, y + rect.Y].frameY = rect[x, y].FrameY; - } + Main.tile[x + rect.X, y + rect.Y].frameX = rect[x, y].FrameX; + Main.tile[x + rect.X, y + rect.Y].frameY = rect[x, y].FrameY; } } From d0409db5fb0774a9787627e92d14037e7b696712 Mon Sep 17 00:00:00 2001 From: punchready Date: Tue, 4 Apr 2023 06:16:44 +0200 Subject: [PATCH 05/27] Never send back too large tile rects in handling --- .../Handlers/SendTileRectHandlerRefactor.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs index a3c87100f..1472423f5 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -416,6 +416,16 @@ public void OnReceive(object sender, GetDataHandlers.SendTileRectEventArgs args) // this handler handles the entire logic of this packet args.Handled = true; + // as of 1.4 this is the biggest size the client will send in any case, determined by full code analysis + // see default matches above and special cases below + if (args.Width > 4 || args.Length > 4) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from size from {args.Player.Name}")); + + // definitely invalid; do not send any correcting data + return; + } + // player throttled? if (args.Player.IsBouncerThrottled()) { @@ -436,16 +446,6 @@ public void OnReceive(object sender, GetDataHandlers.SendTileRectEventArgs args) return; } - // as of 1.4 this is the biggest size the client will send in any case, determined by full code analysis - // see default matches above and special cases below - if (args.Width > 4 || args.Length > 4) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from non-vanilla tilemod from {args.Player.Name}")); - - // definitely invalid; do not send any correcting data - return; - } - // read the tile rectangle TileRect rect = TileRect.Read(args.Data, args.TileX, args.TileY, args.Width, args.Length); From c309990f9488af07939111e5bc153cdcd571e25b Mon Sep 17 00:00:00 2001 From: punchready Date: Wed, 5 Apr 2023 06:43:47 +0200 Subject: [PATCH 06/27] Fix LunarMonolith toggling --- TShockAPI/Handlers/SendTileRectHandlerRefactor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs index 1472423f5..b3a2d1e7d 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -211,10 +211,14 @@ public bool Matches(TSPlayer player, TileRect rect) { if (tile.FrameY < 0 || tile.FrameY > MaxFrameY || tile.FrameY % FrameYStep != 0) { + // this is the only tile type sent in a tile rect where the frame have a different pattern (56, 74, 92 instead of 54, 72, 90) + if (!(TileType == TileID.LunarMonolith && tile.FrameY % FrameYStep == 2)) + { return false; } } } + } if (Type == MatchType.Removal) { if (tile.Active) From b57eb91230c9cff20c2249236355953a99339ba7 Mon Sep 17 00:00:00 2001 From: punchready Date: Wed, 5 Apr 2023 06:44:21 +0200 Subject: [PATCH 07/27] Rewrite conversion spread handling to be much more accurate --- .../Handlers/SendTileRectHandlerRefactor.cs | 576 ++++++++++++++++-- 1 file changed, 511 insertions(+), 65 deletions(-) diff --git a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs index b3a2d1e7d..34f8a4959 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -214,11 +214,11 @@ public bool Matches(TSPlayer player, TileRect rect) // this is the only tile type sent in a tile rect where the frame have a different pattern (56, 74, 92 instead of 54, 72, 90) if (!(TileType == TileID.LunarMonolith && tile.FrameY % FrameYStep == 2)) { - return false; + return false; + } } } } - } if (Type == MatchType.Removal) { if (tile.Active) @@ -578,7 +578,7 @@ private static bool IsRectDistanceValid(TSPlayer player, TileRect rect) /// - /// Checks whether the tile rect is a valid conversion spread (Clentaminator, Powders, etc.) + /// Checks whether the tile rect is a valid conversion spread (Clentaminator, Powders, etc.). /// /// The player the operation originates from. /// The tile rectangle of the operation. @@ -593,81 +593,48 @@ private static bool MatchesConversionSpread(TSPlayer player, TileRect rect) ITile oldTile = Main.tile[rect.X, rect.Y]; NetTile newTile = rect[0, 0]; - bool matchedTileOrWall = false; + WorldGenMock.SimulateConversionChange(rect.X, rect.Y, out HashSet validTiles, out HashSet validWalls); - if (oldTile.active()) + if (newTile.Type != oldTile.type && validTiles.Contains(newTile.Type)) { - if ( - ( - (TileID.Sets.Conversion.Stone[oldTile.type] || Main.tileMoss[oldTile.type]) && - (TileID.Sets.Conversion.Stone[newTile.Type] || Main.tileMoss[newTile.Type]) - ) || - ( - (oldTile.type == TileID.Dirt || oldTile.type == TileID.Mud) && - (newTile.Type == TileID.Dirt || newTile.Type == TileID.Mud) - ) || - TileID.Sets.Conversion.Grass[oldTile.type] && TileID.Sets.Conversion.Grass[newTile.Type] || - TileID.Sets.Conversion.Ice[oldTile.type] && TileID.Sets.Conversion.Ice[newTile.Type] || - TileID.Sets.Conversion.Sand[oldTile.type] && TileID.Sets.Conversion.Sand[newTile.Type] || - TileID.Sets.Conversion.Sandstone[oldTile.type] && TileID.Sets.Conversion.Sandstone[newTile.Type] || - TileID.Sets.Conversion.HardenedSand[oldTile.type] && TileID.Sets.Conversion.HardenedSand[newTile.Type] || - TileID.Sets.Conversion.Thorn[oldTile.type] && TileID.Sets.Conversion.Thorn[newTile.Type] || - TileID.Sets.Conversion.Moss[oldTile.type] && TileID.Sets.Conversion.Moss[newTile.Type] || - TileID.Sets.Conversion.MossBrick[oldTile.type] && TileID.Sets.Conversion.MossBrick[newTile.Type] - ) + if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) { - if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - matchedTileOrWall = true; - } - else if (!player.HasBuildPermission(rect.X, rect.Y)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - matchedTileOrWall = true; - } - else - { - Main.tile[rect.X, rect.Y].type = newTile.Type; - Main.tile[rect.X, rect.Y].frameX = newTile.FrameX; - Main.tile[rect.X, rect.Y].frameY = newTile.FrameY; + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + else if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + else + { + Main.tile[rect.X, rect.Y].type = newTile.Type; + Main.tile[rect.X, rect.Y].frameX = newTile.FrameX; + Main.tile[rect.X, rect.Y].frameY = newTile.FrameY; - matchedTileOrWall = true; - } + return true; } } - if (oldTile.wall != 0) + if (newTile.Wall != oldTile.wall && validWalls.Contains(newTile.Wall)) { - if ( - WallID.Sets.Conversion.Stone[oldTile.wall] && WallID.Sets.Conversion.Stone[newTile.Wall] || - WallID.Sets.Conversion.Grass[oldTile.wall] && WallID.Sets.Conversion.Grass[newTile.Wall] || - WallID.Sets.Conversion.Sandstone[oldTile.wall] && WallID.Sets.Conversion.Sandstone[newTile.Wall] || - WallID.Sets.Conversion.HardenedSand[oldTile.wall] && WallID.Sets.Conversion.HardenedSand[newTile.Wall] || - WallID.Sets.Conversion.PureSand[oldTile.wall] && WallID.Sets.Conversion.PureSand[newTile.Wall] || - WallID.Sets.Conversion.NewWall1[oldTile.wall] && WallID.Sets.Conversion.NewWall1[newTile.Wall] || - WallID.Sets.Conversion.NewWall2[oldTile.wall] && WallID.Sets.Conversion.NewWall2[newTile.Wall] || - WallID.Sets.Conversion.NewWall3[oldTile.wall] && WallID.Sets.Conversion.NewWall3[newTile.Wall] || - WallID.Sets.Conversion.NewWall4[oldTile.wall] && WallID.Sets.Conversion.NewWall4[newTile.Wall] - ) - { - // wallbans when? + // wallbans when? - if (!player.HasBuildPermission(rect.X, rect.Y)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - matchedTileOrWall = true; - } - else - { - Main.tile[rect.X, rect.Y].wall = newTile.Wall; + if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + else + { + Main.tile[rect.X, rect.Y].wall = newTile.Wall; - matchedTileOrWall = true; - } + return true; } } - return matchedTileOrWall; + return false; } @@ -881,4 +848,483 @@ private static bool MatchesChristmasTree(TSPlayer player, TileRect rect) return false; } } + + /// + /// This helper class allows simulating a `WorldGen.Convert` call and retrieving all valid changes for a given tile. + /// + internal static class WorldGenMock + { + /// + /// This is a mock tile which collects all possible changes the `WorldGen.Convert` code could make in its property setters. + /// + private sealed class MockTile + { + private readonly HashSet _setTypes; + private readonly HashSet _setWalls; + + private ushort _type; + private ushort _wall; + + public MockTile(ushort type, ushort wall, HashSet setTypes, HashSet setWalls) + { + _setTypes = setTypes; + _setWalls = setWalls; + _type = type; + _wall = wall; + } + +#pragma warning disable IDE1006 + + public ushort type + { + get => _type; + set + { + _setTypes.Add(value); + _type = value; + } + } + + public ushort wall + { + get => _wall; + set + { + _setWalls.Add(value); + _wall = value; + } + } + +#pragma warning restore IDE1006 + } + + /// + /// Simulates what would happen if `WorldGen.Convert` was called on the given coordinates and returns two sets with the possible tile type and wall types that the conversion could change the tile to. + /// + public static void SimulateConversionChange(int x, int y, out HashSet validTiles, out HashSet validWalls) + { + validTiles = new HashSet(); + validWalls = new HashSet(); + + // all the conversion types used in the code, most apparent in Projectile ai 31 + foreach (int conversionType in new int[] { 0, 1, 2, 3, 4, 5, 6, 7 }) + { + MockTile mock = new(Main.tile[x, y].type, Main.tile[x, y].wall, validTiles, validWalls); + Convert(mock, x, y, conversionType); + } + } + + /* + * This is a copy of the `WorldGen.Convert` method with the following precise changes: + * - Added a `MockTile tile` parameter + * - Changed the `i` and `j` parameters to `k` and `l` + * - Removed the size parameter + * - Removed the area loop and `Tile tile = Main.tile[k, l]` access in favor of using the tile parameter + * - Removed all calls to `WorldGen.SquareWallFrame`, `NetMessage.SendTileSquare`, `WorldGen.TryKillingTreesAboveIfTheyWouldBecomeInvalid` + * - Changed all `continue` statements to `break` statements + * - Removed the ifs checking the bounds of the tile and wall types + * - Removed branches that would call `WorldGen.KillTile` + * - Changed branches depending on randomness to instead set the property to both values after one another + * + * This overall leads to a method that can be called on a MockTile and real-world coordinates and will spit out the proper conversion changes into the MockTile. + */ + + private static void Convert(MockTile tile, int k, int l, int conversionType) + { + int type = tile.type; + int wall = tile.wall; + switch (conversionType) + { + case 4: + if (WallID.Sets.Conversion.Grass[wall] && wall != 81) + { + tile.wall = 81; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall != 83) + { + tile.wall = 83; + } + else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 218) + { + tile.wall = 218; + } + else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 221) + { + tile.wall = 221; + } + else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 192) + { + tile.wall = 192; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 193) + { + tile.wall = 193; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 194) + { + tile.wall = 194; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 195) + { + tile.wall = 195; + } + if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type]) && type != 203) + { + tile.type = 203; + } + else if (TileID.Sets.Conversion.JungleGrass[type] && type != 662) + { + tile.type = 662; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 199) + { + tile.type = 199; + } + else if (TileID.Sets.Conversion.Ice[type] && type != 200) + { + tile.type = 200; + } + else if (TileID.Sets.Conversion.Sand[type] && type != 234) + { + tile.type = 234; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 399) + { + tile.type = 399; + } + else if (TileID.Sets.Conversion.Sandstone[type] && type != 401) + { + tile.type = 401; + } + else if (TileID.Sets.Conversion.Thorn[type] && type != 352) + { + tile.type = 352; + } + break; + case 2: + if (WallID.Sets.Conversion.Grass[wall] && wall != 70) + { + tile.wall = 70; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall != 28) + { + tile.wall = 28; + } + else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 219) + { + tile.wall = 219; + } + else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 222) + { + tile.wall = 222; + } + else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 200) + { + tile.wall = 200; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 201) + { + tile.wall = 201; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 202) + { + tile.wall = 202; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 203) + { + tile.wall = 203; + } + if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type]) && type != 117) + { + tile.type = 117; + } + else if (TileID.Sets.Conversion.GolfGrass[type] && type != 492) + { + tile.type = 492; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 109 && type != 492) + { + tile.type = 109; + } + else if (TileID.Sets.Conversion.Ice[type] && type != 164) + { + tile.type = 164; + } + else if (TileID.Sets.Conversion.Sand[type] && type != 116) + { + tile.type = 116; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 402) + { + tile.type = 402; + } + else if (TileID.Sets.Conversion.Sandstone[type] && type != 403) + { + tile.type = 403; + } + if (type == 59 && (Main.tile[k - 1, l].type == 109 || Main.tile[k + 1, l].type == 109 || Main.tile[k, l - 1].type == 109 || Main.tile[k, l + 1].type == 109)) + { + tile.type = 0; + } + break; + case 1: + if (WallID.Sets.Conversion.Grass[wall] && wall != 69) + { + tile.wall = 69; + } + else if (TileID.Sets.Conversion.JungleGrass[type] && type != 661) + { + tile.type = 661; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall != 3) + { + tile.wall = 3; + } + else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 217) + { + tile.wall = 217; + } + else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 220) + { + tile.wall = 220; + } + else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 188) + { + tile.wall = 188; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 189) + { + tile.wall = 189; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 190) + { + tile.wall = 190; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 191) + { + tile.wall = 191; + } + if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type]) && type != 25) + { + tile.type = 25; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 23) + { + tile.type = 23; + } + else if (TileID.Sets.Conversion.Ice[type] && type != 163) + { + tile.type = 163; + } + else if (TileID.Sets.Conversion.Sand[type] && type != 112) + { + tile.type = 112; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 398) + { + tile.type = 398; + } + else if (TileID.Sets.Conversion.Sandstone[type] && type != 400) + { + tile.type = 400; + } + else if (TileID.Sets.Conversion.Thorn[type] && type != 32) + { + tile.type = 32; + } + break; + case 3: + if (WallID.Sets.CanBeConvertedToGlowingMushroom[wall]) + { + tile.wall = 80; + } + if (tile.type == 60) + { + tile.type = 70; + } + break; + case 5: + if ((WallID.Sets.Conversion.Stone[wall] || WallID.Sets.Conversion.NewWall1[wall] || WallID.Sets.Conversion.NewWall2[wall] || WallID.Sets.Conversion.NewWall3[wall] || WallID.Sets.Conversion.NewWall4[wall] || WallID.Sets.Conversion.Ice[wall] || WallID.Sets.Conversion.Sandstone[wall]) && wall != 187) + { + tile.wall = 187; + } + else if ((WallID.Sets.Conversion.HardenedSand[wall] || WallID.Sets.Conversion.Dirt[wall] || WallID.Sets.Conversion.Snow[wall]) && wall != 216) + { + tile.wall = 216; + } + if ((TileID.Sets.Conversion.Grass[type] || TileID.Sets.Conversion.Sand[type] || TileID.Sets.Conversion.Snow[type] || TileID.Sets.Conversion.Dirt[type]) && type != 53) + { + int num = 53; + if (WorldGen.BlockBelowMakesSandConvertIntoHardenedSand(k, l)) + { + num = 397; + } + tile.type = (ushort)num; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 397) + { + tile.type = 397; + } + else if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type] || TileID.Sets.Conversion.Ice[type] || TileID.Sets.Conversion.Sandstone[type]) && type != 396) + { + tile.type = 396; + } + break; + case 6: + if ((WallID.Sets.Conversion.Stone[wall] || WallID.Sets.Conversion.NewWall1[wall] || WallID.Sets.Conversion.NewWall2[wall] || WallID.Sets.Conversion.NewWall3[wall] || WallID.Sets.Conversion.NewWall4[wall] || WallID.Sets.Conversion.Ice[wall] || WallID.Sets.Conversion.Sandstone[wall]) && wall != 71) + { + tile.wall = 71; + } + else if ((WallID.Sets.Conversion.HardenedSand[wall] || WallID.Sets.Conversion.Dirt[wall] || WallID.Sets.Conversion.Snow[wall]) && wall != 40) + { + tile.wall = 40; + } + if ((TileID.Sets.Conversion.Grass[type] || TileID.Sets.Conversion.Sand[type] || TileID.Sets.Conversion.HardenedSand[type] || TileID.Sets.Conversion.Snow[type] || TileID.Sets.Conversion.Dirt[type]) && type != 147) + { + tile.type = 147; + } + else if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type] || TileID.Sets.Conversion.Ice[type] || TileID.Sets.Conversion.Sandstone[type]) && type != 161) + { + tile.type = 161; + } + break; + case 7: + if ((WallID.Sets.Conversion.Stone[wall] || WallID.Sets.Conversion.Ice[wall] || WallID.Sets.Conversion.Sandstone[wall]) && wall != 1) + { + tile.wall = 1; + } + else if ((WallID.Sets.Conversion.HardenedSand[wall] || WallID.Sets.Conversion.Snow[wall] || WallID.Sets.Conversion.Dirt[wall]) && wall != 2) + { + tile.wall = 2; + } + else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 196) + { + tile.wall = 196; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 197) + { + tile.wall = 197; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 198) + { + tile.wall = 198; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 199) + { + tile.wall = 199; + } + if ((TileID.Sets.Conversion.Stone[type] || TileID.Sets.Conversion.Ice[type] || TileID.Sets.Conversion.Sandstone[type]) && type != 1) + { + tile.type = 1; + } + else if (TileID.Sets.Conversion.GolfGrass[type] && type != 477) + { + tile.type = 477; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 2 && type != 477) + { + tile.type = 2; + } + else if ((TileID.Sets.Conversion.Sand[type] || TileID.Sets.Conversion.HardenedSand[type] || TileID.Sets.Conversion.Snow[type] || TileID.Sets.Conversion.Dirt[type]) && type != 0) + { + int num2 = 0; + if (WorldGen.TileIsExposedToAir(k, l)) + { + num2 = 2; + } + tile.type = (ushort)num2; + } + break; + } + if (tile.wall == 69 || tile.wall == 70 || tile.wall == 81) + { + if (l < Main.worldSurface) + { + tile.wall = 65; + tile.wall = 63; + } + else + { + tile.wall = 64; + } + } + else if (WallID.Sets.Conversion.Stone[wall] && wall != 1 && wall != 262 && wall != 274 && wall != 61 && wall != 185) + { + tile.wall = 1; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall == 262) + { + tile.wall = 61; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall == 274) + { + tile.wall = 185; + } + if (WallID.Sets.Conversion.NewWall1[wall] && wall != 212) + { + tile.wall = 212; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 213) + { + tile.wall = 213; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 214) + { + tile.wall = 214; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 215) + { + tile.wall = 215; + } + else if (tile.wall == 80) + { + tile.wall = 15; + tile.wall = 64; + } + else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 216) + { + tile.wall = 216; + } + else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 187) + { + tile.wall = 187; + } + if (tile.type == 492) + { + tile.type = 477; + } + else if (TileID.Sets.Conversion.JungleGrass[type] && type != 60) + { + tile.type = 60; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 2 && type != 477) + { + tile.type = 2; + } + else if (TileID.Sets.Conversion.Stone[type] && type != 1) + { + tile.type = 1; + } + else if (TileID.Sets.Conversion.Sand[type] && type != 53) + { + tile.type = 53; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 397) + { + tile.type = 397; + } + else if (TileID.Sets.Conversion.Sandstone[type] && type != 396) + { + tile.type = 396; + } + else if (TileID.Sets.Conversion.Ice[type] && type != 161) + { + tile.type = 161; + } + else if (TileID.Sets.Conversion.MushroomGrass[type]) + { + tile.type = 60; + } + } + } } From 0dd15277e471a00e2a714bf1710698ecfc514fee Mon Sep 17 00:00:00 2001 From: punchready Date: Tue, 9 May 2023 12:41:54 +0200 Subject: [PATCH 08/27] Remove old STR handler --- TShockAPI/Bouncer.cs | 4 +- TShockAPI/Handlers/SendTileRectHandler.cs | 1658 +++++++++++------ .../Handlers/SendTileRectHandlerRefactor.cs | 1330 ------------- 3 files changed, 1126 insertions(+), 1866 deletions(-) delete mode 100644 TShockAPI/Handlers/SendTileRectHandlerRefactor.cs diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs index c7c72f968..6ff7fd1aa 100644 --- a/TShockAPI/Bouncer.cs +++ b/TShockAPI/Bouncer.cs @@ -35,7 +35,7 @@ namespace TShockAPI /// Bouncer is the TShock anti-hack and anti-cheat system. internal sealed class Bouncer { - internal Handlers.SendTileRectHandlerRefactor STSHandler { get; private set; } + internal Handlers.SendTileRectHandler STSHandler { get; private set; } internal Handlers.NetModules.NetModulePacketHandler NetModuleHandler { get; private set; } internal Handlers.EmojiHandler EmojiHandler { get; private set; } internal Handlers.IllegalPerSe.EmojiPlayerMismatch EmojiPlayerMismatch { get; private set; } @@ -83,7 +83,7 @@ internal class BuffLimit /// A new Bouncer. internal Bouncer() { - STSHandler = new Handlers.SendTileRectHandlerRefactor(); + STSHandler = new Handlers.SendTileRectHandler(); GetDataHandlers.SendTileRect += STSHandler.OnReceive; NetModuleHandler = new Handlers.NetModules.NetModulePacketHandler(); diff --git a/TShockAPI/Handlers/SendTileRectHandler.cs b/TShockAPI/Handlers/SendTileRectHandler.cs index 80d5a47c4..64d9d8a79 100644 --- a/TShockAPI/Handlers/SendTileRectHandler.cs +++ b/TShockAPI/Handlers/SendTileRectHandler.cs @@ -1,739 +1,1329 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; +using System.IO; using Terraria; -using Terraria.DataStructures; -using Terraria.GameContent.Tile_Entities; using Terraria.ID; -using Terraria.ObjectData; using TShockAPI.Net; namespace TShockAPI.Handlers { /// - /// Provides processors for handling Tile Rect packets + /// Provides processors for handling tile rect packets. + /// This required many hours of reverse engineering work, and is kindly provided to TShock for free by @punchready. /// - public class SendTileRectHandler : IPacketHandler + public sealed class SendTileRectHandler : IPacketHandler { /// - /// Maps plant tile types to their valid grass ground tiles when using flower boots + /// Represents a tile rectangle sent through the packet. /// - private static readonly Dictionary> FlowerBootPlantToGrassMap = new Dictionary> + private sealed class TileRect { - { TileID.Plants, new HashSet() - { - TileID.Grass, TileID.GolfGrass - } }, - { TileID.HallowedPlants, new HashSet() - { - TileID.HallowedGrass, TileID.GolfGrassHallowed - } }, - { TileID.HallowedPlants2, new HashSet() - { - TileID.HallowedGrass, TileID.GolfGrassHallowed - } }, - { TileID.JunglePlants2, new HashSet() + private readonly NetTile[,] _tiles; + public readonly int X; + public readonly int Y; + public readonly int Width; + public readonly int Height; + + /// + /// Accesses the tiles contained in this rect. + /// + /// The X coordinate within the rect. + /// The Y coordinate within the rect. + /// The tile at the given position within the rect. + public NetTile this[int x, int y] => _tiles[x, y]; + + /// + /// Constructs a new tile rect based on the given information. + /// + public TileRect(NetTile[,] tiles, int x, int y, int width, int height) { - TileID.JungleGrass - } }, - { TileID.AshPlants, new HashSet() + _tiles = tiles; + X = x; + Y = y; + Width = width; + Height = height; + } + + /// + /// Reads a tile rect from the given stream. + /// + /// The resulting tile rect. + public static TileRect Read(MemoryStream stream, int tileX, int tileY, int width, int height) { - TileID.AshGrass - } }, - }; + NetTile[,] tiles = new NetTile[width, height]; + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + tiles[x, y] = new NetTile(); + tiles[x, y].Unpack(stream); // explicit > implicit + } + } + return new TileRect(tiles, tileX, tileY, width, height); + } + } /// - /// Maps plant tile types to a list of valid styles, which are used to determine the FrameX value of the plant tile - /// See `Player.DoBootsEffect_PlaceFlowersOnTile` + /// Represents a common tile rect operation (Placement, State Change, Removal). /// - private static readonly Dictionary> FlowerBootPlantToStyleMap = new Dictionary>() + private readonly struct TileRectMatch { - { TileID.Plants, new HashSet() - { - // The upper line is from a `NextFromList` call - // The lower line is from an additional switch which will add the listed options by adding a random value to a select set of styles - 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 24, 27, 30, 33, 36, 39, 42, - 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, 44, - } }, - { TileID.HallowedPlants, new HashSet() - { - // 5 is intentionally missing here because it is being skipped by vanilla - 4, 6, - } }, - { TileID.HallowedPlants2, new HashSet() - { - // 5 is intentionally missing here because it is being skipped by vanilla - 2, 3, 4, 6, 7, - } }, - { TileID.JunglePlants2, new HashSet() - { - 9, 10, 11, 12, 13, 14, 15, 16, - } }, - { TileID.AshPlants, new HashSet() + private const short IGNORE_FRAME = -1; + + private enum MatchType { - 6, 7, 8, 9, 10, - } }, - }; + Placement, + StateChange, + Removal, + } - /// - /// Item IDs that can spawn flowers while you walk - /// - public static List FlowerBootItems = new List - { - ItemID.FlowerBoots, - ItemID.FairyBoots - }; + private readonly int Width; + private readonly int Height; - /// - /// Maps TileIDs to Tile Entity IDs. - /// Note: is empty at the time of writing, but entities are dynamically assigned their ID at initialize time - /// which is why we can use the _myEntityId field on each entity type - /// - public static Dictionary TileEntityIdToTileIdMap = new Dictionary - { - { TileID.TargetDummy, TETrainingDummy._myEntityID }, - { TileID.ItemFrame, TEItemFrame._myEntityID }, - { TileID.LogicSensor, TELogicSensor._myEntityID }, - { TileID.DisplayDoll, TEDisplayDoll._myEntityID }, - { TileID.WeaponsRack2, TEWeaponsRack._myEntityID }, - { TileID.HatRack, TEHatRack._myEntityID }, - { TileID.FoodPlatter, TEFoodPlatter._myEntityID }, - { TileID.TeleportationPylon, TETeleportationPylon._myEntityID } - }; + private readonly ushort TileType; + private readonly short MaxFrameX; + private readonly short MaxFrameY; + private readonly short FrameXStep; + private readonly short FrameYStep; - /// - /// Invoked when a SendTileRect packet is received - /// - /// - /// - public void OnReceive(object sender, GetDataHandlers.SendTileRectEventArgs args) - { - // By default, we'll handle everything - args.Handled = true; + private readonly MatchType Type; - if (ShouldSkipProcessing(args)) + private TileRectMatch(MatchType type, int width, int height, ushort tileType, short maxFrameX, short maxFrameY, short frameXStep, short frameYStep) { - return; + Type = type; + Width = width; + Height = height; + TileType = tileType; + MaxFrameX = maxFrameX; + MaxFrameY = maxFrameY; + FrameXStep = frameXStep; + FrameYStep = frameYStep; } - bool[,] processed = new bool[args.Width, args.Length]; - NetTile[,] tiles = ReadNetTilesFromStream(args.Data, args.Width, args.Length); - - Debug.VisualiseTileSetDiff(args.TileX, args.TileY, args.Width, args.Length, tiles); + /// + /// Creates a new placement operation. + /// + /// The width of the placement. + /// The height of the placement. + /// The tile type of the placement. + /// The maximum allowed frameX of the placement. + /// The maximum allowed frameY of the placement. + /// The step size in which frameX changes for this placement, or 1 if any value is allowed. + /// The step size in which frameX changes for this placement, or 1 if any value is allowed. + /// The resulting operation match. + public static TileRectMatch Placement(int width, int height, ushort tileType, short maxFrameX, short maxFrameY, short frameXStep, short frameYStep) + { + return new TileRectMatch(MatchType.Placement, width, height, tileType, maxFrameX, maxFrameY, frameXStep, frameYStep); + } - IterateTileRect(tiles, processed, args); + /// + /// Creates a new state change operation. + /// + /// The width of the state change. + /// The height of the state change. + /// The target tile type of the state change. + /// The maximum allowed frameX of the state change. + /// The maximum allowed frameY of the state change. + /// The step size in which frameX changes for this placement, or 1 if any value is allowed. + /// The step size in which frameY changes for this placement, or 1 if any value is allowed. + /// The resulting operation match. + public static TileRectMatch StateChange(int width, int height, ushort tileType, short maxFrameX, short maxFrameY, short frameXStep, short frameYStep) + { + return new TileRectMatch(MatchType.StateChange, width, height, tileType, maxFrameX, maxFrameY, frameXStep, frameYStep); + } - // Uncommenting this function will send the same tile rect 10 blocks above you for visualisation. This will modify your world and overwrite existing blocks. - // Use in test worlds only. - //Debug.DisplayTileSetInGame(args.TileX, (short)(args.TileY - 10), args.Width, args.Length, tiles, args.Player); + /// + /// Creates a new state change operation which only changes frameX. + /// + /// The width of the state change. + /// The height of the state change. + /// The target tile type of the state change. + /// The maximum allowed frameX of the state change. + /// The step size in which frameX changes for this placement, or 1 if any value is allowed. + /// The resulting operation match. + public static TileRectMatch StateChangeX(int width, int height, ushort tileType, short maxFrame, short frameStep) + { + return new TileRectMatch(MatchType.StateChange, width, height, tileType, maxFrame, IGNORE_FRAME, frameStep, 0); + } - // If we are handling this event then we have updated the server's Main.tile state the way we want it. - // At this point we should send our state back to the client so they remain in sync with the server - if (args.Handled == true) + /// + /// Creates a new state change operation which only changes frameY. + /// + /// The width of the state change. + /// The height of the state change. + /// The target tile type of the state change. + /// The maximum allowed frameY of the state change. + /// The step size in which frameY changes for this placement, or 1 if any value is allowed. + /// The resulting operation match. + public static TileRectMatch StateChangeY(int width, int height, ushort tileType, short maxFrame, short frameStep) { - TSPlayer.All.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from carbonara from {args.Player.Name}")); + return new TileRectMatch(MatchType.StateChange, width, height, tileType, IGNORE_FRAME, maxFrame, 0, frameStep); } - } - /// - /// Iterates over each tile in the tile rectangle and performs processing on individual tiles or multi-tile Tile Objects - /// - /// - /// - /// - internal void IterateTileRect(NetTile[,] tiles, bool[,] processed, GetDataHandlers.SendTileRectEventArgs args) - { - int tileX = args.TileX; - int tileY = args.TileY; - byte width = args.Width; - byte length = args.Length; + /// + /// Creates a new removal operation. + /// + /// The width of the removal. + /// The height of the removal. + /// The target tile type of the removal. + /// The resulting operation match. + public static TileRectMatch Removal(int width, int height, ushort tileType) + { + return new TileRectMatch(MatchType.Removal, width, height, tileType, 0, 0, 0, 0); + } - for (int x = 0; x < width; x++) + /// + /// Determines whether the given tile rectangle matches this operation, and if so, applies it to the world. + /// + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches this operation and the changes have been applied, otherwise . + public bool Matches(TSPlayer player, TileRect rect) { - for (int y = 0; y < length; y++) + if (rect.Width != Width || rect.Height != Height) + { + return false; + } + + for (int x = 0; x < rect.Width; x++) { - // Do not process already processed tiles - if (processed[x, y]) + for (int y = 0; y < rect.Height; y++) { - continue; + NetTile tile = rect[x, y]; + if (Type is MatchType.Placement or MatchType.StateChange) + { + if (tile.Type != TileType) + { + return false; + } + } + if (Type is MatchType.Placement or MatchType.StateChange) + { + if (MaxFrameX != IGNORE_FRAME) + { + if (tile.FrameX < 0 || tile.FrameX > MaxFrameX || tile.FrameX % FrameXStep != 0) + { + return false; + } + } + if (MaxFrameY != IGNORE_FRAME) + { + if (tile.FrameY < 0 || tile.FrameY > MaxFrameY || tile.FrameY % FrameYStep != 0) + { + // this is the only tile type sent in a tile rect where the frame have a different pattern (56, 74, 92 instead of 54, 72, 90) + if (!(TileType == TileID.LunarMonolith && tile.FrameY % FrameYStep == 2)) + { + return false; + } + } + } + } + if (Type == MatchType.Removal) + { + if (tile.Active) + { + return false; + } + } } + } - int realX = tileX + x; - int realY = tileY + y; - - // Do not process tiles outside of the world boundaries - if ((realX < 0 || realX >= Main.maxTilesX) - || (realY < 0 || realY > Main.maxTilesY)) + for (int x = rect.X; x < rect.X + rect.Width; x++) + { + for (int y = rect.Y; y < rect.Y + rect.Height; y++) { - processed[x, y] = true; - continue; + if (!player.HasBuildPermission(x, y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } } + } + + switch (Type) + { + case MatchType.Placement: + { + return MatchPlacement(player, rect); + } + case MatchType.StateChange: + { + return MatchStateChange(player, rect); + } + case MatchType.Removal: + { + return MatchRemoval(player, rect); + } + } + + return false; + } - // Do not process tiles that the player cannot update - if (!args.Player.HasBuildPermission(realX, realY) || - !args.Player.IsInRange(realX, realY)) + private bool MatchPlacement(TSPlayer player, TileRect rect) + { + for (int x = rect.X; x < rect.Y + rect.Width; x++) + { + for (int y = rect.Y; y < rect.Y + rect.Height; y++) { - processed[x, y] = true; - continue; + if (Main.tile[x, y].active() && !(Main.tile[x, y].type != TileID.RollingCactus && (Main.tileCut[Main.tile[x, y].type] || TileID.Sets.BreakableWhenPlacing[Main.tile[x, y].type]))) + { + return false; + } } + } + + // let's hope tile types never go out of short range (they use ushort in terraria's code) + if (TShock.TileBans.TileIsBanned((short)TileType, player)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } - NetTile newTile = tiles[x, y]; + for (int x = 0; x < rect.Width; x++) + { + for (int y = 0; y < rect.Height; y++) + { + Main.tile[x + rect.X, y + rect.Y].active(active: true); + Main.tile[x + rect.X, y + rect.Y].type = rect[x, y].Type; + Main.tile[x + rect.X, y + rect.Y].frameX = rect[x, y].FrameX; + Main.tile[x + rect.X, y + rect.Y].frameY = rect[x, y].FrameY; + } + } - TileObjectData data; + return true; + } - // If the new tile has an associated TileObjectData object, we take the tile and the surrounding tiles that make up the tile object - // and process them as a tile object - if (newTile.Type < TileObjectData._data.Count && TileObjectData._data[newTile.Type] != null) + private bool MatchStateChange(TSPlayer player, TileRect rect) + { + for (int x = rect.X; x < rect.Y + rect.Width; x++) + { + for (int y = rect.Y; y < rect.Y + rect.Height; y++) { - // Verify that the changes are actually valid conceptually - // Many tiles that are never placed or modified using this packet are valid TileObjectData entries, which is the main attack vector for most exploits using this packet - if (Main.tile[realX, realY].type == newTile.Type) + if (!Main.tile[x, y].active() || Main.tile[x, y].type != TileType) { - switch (newTile.Type) - { - // Some individual cases might still allow crashing exploits, as the actual framing is not being checked here - // Doing so requires hard-coding the individual valid framing values and is a lot of effort - case TileID.ProjectilePressurePad: - case TileID.WirePipe: - case TileID.Traps: - case TileID.Candles: - case TileID.PeaceCandle: - case TileID.WaterCandle: - case TileID.PlatinumCandle: - case TileID.Firework: - case TileID.WaterFountain: - case TileID.BloodMoonMonolith: - case TileID.VoidMonolith: - case TileID.LunarMonolith: - case TileID.MusicBoxes: - case TileID.ArrowSign: - case TileID.PaintedArrowSign: - case TileID.Cannon: - case TileID.Campfire: - case TileID.Plants: - case TileID.MinecartTrack: - case TileID.ChristmasTree: - case TileID.ShimmerMonolith: - { - // Allowed changes - - // Based on empirical tests, these should be some conservative upper bounds for framing values - if (newTile.FrameX != -1 || newTile.FrameY != -1) - { - if (newTile.FrameX is < 0 or > 1000) - { - processed[x, y] = true; - continue; - } - if (newTile.FrameY is < 0 or > 5000) - { - processed[x, y] = true; - continue; - } - } - } - break; - default: - { - processed[x, y] = true; - continue; - } - } + return false; } - else + } + } + + for (int x = 0; x < rect.Width; x++) + { + for (int y = 0; y < rect.Height; y++) + { + if (MaxFrameX != IGNORE_FRAME) { - // Together with Flower Boots and Land Mine destruction, these are the only cases where a tile type is allowed to be modified - switch (newTile.Type) - { - case TileID.LogicSensor: - case TileID.FoodPlatter: - case TileID.WeaponsRack2: - case TileID.ItemFrame: - case TileID.HatRack: - case TileID.DisplayDoll: - case TileID.TeleportationPylon: - case TileID.TargetDummy: - { - // Allowed placements - - // Based on empirical tests, these should be some conservative upper bounds for framing values - if (newTile.FrameX != -1 || newTile.FrameY != -1) - { - if (newTile.FrameX is < 0 or > 1000) - { - processed[x, y] = true; - continue; - } - if (newTile.FrameY is < 0 or > 500) - { - processed[x, y] = true; - continue; - } - } - } - break; - default: - { - processed[x, y] = true; - continue; - } - } + Main.tile[x + rect.X, y + rect.Y].frameX = rect[x, y].FrameX; } - - data = TileObjectData._data[newTile.Type]; - NetTile[,] newTiles; - int objWidth = data.Width; - int objHeight = data.Height; - - // Ensure the tile object fits inside the rect before processing it - if (!DoesTileObjectFitInTileRect(x, y, objWidth, objHeight, width, length, processed)) + if (MaxFrameY != IGNORE_FRAME) { - continue; + Main.tile[x + rect.X, y + rect.Y].frameY = rect[x, y].FrameY; } + } + } - newTiles = new NetTile[objWidth, objHeight]; + return true; + } - for (int i = 0; i < objWidth; i++) + private bool MatchRemoval(TSPlayer player, TileRect rect) + { + for (int x = rect.X; x < rect.Y + rect.Width; x++) + { + for (int y = rect.Y; y < rect.Y + rect.Height; y++) + { + if (!Main.tile[x, y].active() || Main.tile[x, y].type != TileType) { - for (int j = 0; j < objHeight; j++) - { - newTiles[i, j] = tiles[x + i, y + j]; - processed[x + i, y + j] = true; - } + return false; } - ProcessTileObject(newTile.Type, realX, realY, objWidth, objHeight, newTiles, args); - continue; } + } - // If the new tile does not have an associated tile object, process it as an individual tile - ProcessSingleTile(realX, realY, newTile, width, length, args); - processed[x, y] = true; + for (int x = 0; x < rect.Width; x++) + { + for (int y = 0; y < rect.Height; y++) + { + Main.tile[x + rect.X, y + rect.Y].active(active: false); + Main.tile[x + rect.X, y + rect.Y].frameX = -1; + Main.tile[x + rect.X, y + rect.Y].frameY = -1; + } } + + return true; } } /// - /// Processes a tile object consisting of multiple tiles from the tile rect packet + /// Contains the complete list of valid tile rect operations the game currently performs. + /// + // The matches restrict the tile rects to only place one kind of tile, and only with the given maximum values and step sizes for frameX and frameY. This performs pretty much perfect checks on the data, allowing only valid placements. + // For TileID.MinecartTrack, the data is taken from `Minecart._trackSwitchOptions`, allowing any framing value in this array (currently 0-36). + // For TileID.Plants, it is taken from `ItemID.Sets.flowerPacketInfo[n].stylesOnPurity`, allowing every style multiplied by 18. + // The other operations are based on code analysis and manual observation. + private static readonly TileRectMatch[] Matches = new TileRectMatch[] + { + TileRectMatch.Placement(2, 3, TileID.TargetDummy, 54, 36, 18, 18), + TileRectMatch.Placement(3, 4, TileID.TeleportationPylon, 468, 54, 18, 18), + TileRectMatch.Placement(2, 3, TileID.DisplayDoll, 126, 36, 18, 18), + TileRectMatch.Placement(2, 3, TileID.HatRack, 90, 54, 18, 18), + TileRectMatch.Placement(2, 2, TileID.ItemFrame, 162, 18, 18, 18), + TileRectMatch.Placement(3, 3, TileID.WeaponsRack2, 90, 36, 18, 18), + TileRectMatch.Placement(1, 1, TileID.FoodPlatter, 18, 0, 18, 18), + TileRectMatch.Placement(1, 1, TileID.LogicSensor, 108, 0, 18, 18), + + TileRectMatch.StateChangeY(3, 2, TileID.Campfire, 54, 18), + TileRectMatch.StateChangeY(4, 3, TileID.Cannon, 468, 18), + TileRectMatch.StateChangeY(2, 2, TileID.ArrowSign, 270, 18), + TileRectMatch.StateChangeY(2, 2, TileID.PaintedArrowSign, 270, 18), + + TileRectMatch.StateChangeX(2, 2, TileID.MusicBoxes, 54, 18), + + TileRectMatch.StateChangeY(2, 3, TileID.LunarMonolith, 92, 18), + TileRectMatch.StateChangeY(2, 3, TileID.BloodMoonMonolith, 90, 18), + TileRectMatch.StateChangeY(2, 3, TileID.VoidMonolith, 90, 18), + TileRectMatch.StateChangeY(2, 3, TileID.EchoMonolith, 90, 18), + TileRectMatch.StateChangeY(2, 3, TileID.ShimmerMonolith, 144, 18), + TileRectMatch.StateChangeY(2, 4, TileID.WaterFountain, 126, 18), + + TileRectMatch.StateChangeX(1, 1, TileID.Candles, 18, 18), + TileRectMatch.StateChangeX(1, 1, TileID.PeaceCandle, 18, 18), + TileRectMatch.StateChangeX(1, 1, TileID.WaterCandle, 18, 18), + TileRectMatch.StateChangeX(1, 1, TileID.PlatinumCandle, 18, 18), + TileRectMatch.StateChangeX(1, 1, TileID.ShadowCandle, 18, 18), + + TileRectMatch.StateChange(1, 1, TileID.Traps, 90, 90, 18, 18), + + TileRectMatch.StateChangeX(1, 1, TileID.WirePipe, 36, 18), + TileRectMatch.StateChangeX(1, 1, TileID.ProjectilePressurePad, 66, 22), + TileRectMatch.StateChangeX(1, 1, TileID.Plants, 792, 18), + TileRectMatch.StateChangeX(1, 1, TileID.MinecartTrack, 36, 1), + + TileRectMatch.Removal(1, 2, TileID.Firework), + TileRectMatch.Removal(1, 1, TileID.LandMine), + }; + + + /// + /// Handles a packet receive event. /// - /// The tile type the object is comprised of - /// 2D array of NetTile containing the new tiles properties - /// X position at the top left of the object - /// Y position at the top left of the object - /// Width of the tile object - /// Height of the tile object - /// SendTileRectEventArgs containing event information - internal void ProcessTileObject(int tileType, int realX, int realY, int width, int height, NetTile[,] newTiles, GetDataHandlers.SendTileRectEventArgs args) + public void OnReceive(object sender, GetDataHandlers.SendTileRectEventArgs args) { - // As long as the player has permission to build, we should allow a tile object to be placed - // More in depth checks should take place in handlers for the Place Object (79), Update Tile Entity (86), and Place Tile Entity (87) packets - if (!args.Player.HasBuildPermissionForTileObject(realX, realY, width, height)) + // this permission bypasses all checks for direct access to the world + if (args.Player.HasPermission(Permissions.allowclientsideworldedit)) { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from no permission for tile object from {args.Player.Name}")); + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect accepted clientside world edit from {args.Player.Name}")); + + // use vanilla handling + args.Handled = false; return; } - if (TShock.TileBans.TileIsBanned((short)tileType)) + // this handler handles the entire logic of this packet + args.Handled = true; + + // as of 1.4 this is the biggest size the client will send in any case, determined by full code analysis + // see default matches above and special cases below + if (args.Width > 4 || args.Length > 4) { - TShock.Log.ConsoleDebug(GetString("Bouncer / SendTileRect rejected for banned tile")); + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from size from {args.Player.Name}")); + + // definitely invalid; do not send any correcting data return; } - // Update all tiles in the tile object. These will be sent back to the player later - UpdateMultipleServerTileStates(realX, realY, width, height, newTiles); + // player throttled? + if (args.Player.IsBouncerThrottled()) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from throttle from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } - // Tile entities have special placements that we should let the game deal with - if (TileEntityIdToTileIdMap.ContainsKey(tileType)) + // player disabled? + if (args.Player.IsBeingDisabled()) { - TileEntity.PlaceEntityNet(realX, realY, TileEntityIdToTileIdMap[tileType]); + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from being disabled from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; } - } - /// - /// Processes a single tile from the tile rect packet - /// - /// X position at the top left of the object - /// Y position at the top left of the object - /// The NetTile containing new tile properties - /// The width of the rectangle being processed - /// The length of the rectangle being processed - /// SendTileRectEventArgs containing event information - internal void ProcessSingleTile(int realX, int realY, NetTile newTile, byte rectWidth, byte rectLength, GetDataHandlers.SendTileRectEventArgs args) - { - // Some boots allow growing flowers on grass. This process sends a 1x1 tile rect to grow the flowers - // The rect size must be 1 and the player must have an accessory that allows growing flowers in order for this rect to be valid - if (rectWidth == 1 && rectLength == 1 && WorldGen.InWorld(realX, realY + 1) && args.Player.Accessories.Any(a => a != null && FlowerBootItems.Contains(a.type))) + // read the tile rectangle + TileRect rect = TileRect.Read(args.Data, args.TileX, args.TileY, args.Width, args.Length); + + // check if the positioning is valid + if (!IsRectPositionValid(args.Player, rect)) { - ProcessFlowerBoots(realX, realY, newTile); + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from out of bounds / build permission from {args.Player.Name}")); + + // send nothing due to out of bounds return; } - ITile tile = Main.tile[realX, realY]; + // a very special case, due to the clentaminator having a larger range than TSPlayer.IsInRange() allows + if (MatchesConversionSpread(args.Player, rect)) + { + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; + } - // Triggering a single land mine tile - if (rectWidth == 1 && rectLength == 1 && tile.type == TileID.LandMine && !newTile.Active) + // check if the distance is valid + if (!IsRectDistanceValid(args.Player, rect)) { - UpdateServerTileState(tile, newTile, TileDataType.Tile); + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from out of range from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; } - // Hammering a single junction box - if (rectWidth == 1 && rectLength == 1 && tile.type == TileID.WirePipe) + // a very special case, due to the flower seed check otherwise hijacking this + if (MatchesFlowerBoots(args.Player, rect)) { - UpdateServerTileState(tile, newTile, TileDataType.Tile); + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; } - // Mowing a single grass tile: Grass -> GolfGrass OR HallowedGrass -> GolfGrassHallowed - if (rectWidth == 1 && rectLength == 1 && - ( - tile.type == TileID.Grass && newTile.Type == TileID.GolfGrass || - tile.type == TileID.HallowedGrass && newTile.Type == TileID.GolfGrassHallowed - )) + // check if the rect matches any valid operation + foreach (TileRectMatch match in Matches) { - UpdateServerTileState(tile, newTile, TileDataType.Tile); - if (WorldGen.InWorld(realX, realY + 1) && TileID.Sets.IsVine[Main.tile[realX, realY + 1].type]) // vanilla does in theory break the vines on its own, but we can't trust that + if (match.Matches(args.Player, rect)) { - WorldGen.KillTile(realX, realY + 1); + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; } } - // Conversion: only sends a 1x1 rect; has to happen AFTER grass mowing as it would otherwise also let mowing through, but without fixing vines - if (rectWidth == 1 && rectLength == 1) + // a few special cases + if ( + MatchesConversionSpread(args.Player, rect) || + MatchesGrassMow(args.Player, rect) || + MatchesChristmasTree(args.Player, rect) + ) { - ProcessConversionSpreads(tile, newTile); + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; } - // All other single tile updates should not be processed. + TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from matches from {args.Player.Name}")); + + // send correcting data + args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); + return; } /// - /// Applies changes to a tile if a tile rect for flower-growing boots is valid + /// Checks whether the tile rect is at a valid position for the given player. /// - /// The tile x position of the tile rect packet - this is where the flowers are intending to grow - /// The tile y position of the tile rect packet - this is where the flowers are intending to grow - /// The NetTile containing information about the flowers that are being grown - internal void ProcessFlowerBoots(int realX, int realY, NetTile newTile) + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect at a valid position, otherwise . + private static bool IsRectPositionValid(TSPlayer player, TileRect rect) { - ITile tile = Main.tile[realX, realY]; - // Ensure that: - // - the placed plant is valid for the grass below - // - the target tile is empty - // - and the placed plant has valid framing (style * 18 = FrameX) - if ( - FlowerBootPlantToGrassMap.TryGetValue(newTile.Type, out HashSet grassTiles) && - !tile.active() && - grassTiles.Contains(Main.tile[realX, realY + 1].type) && - FlowerBootPlantToStyleMap[newTile.Type].Contains((ushort)(newTile.FrameX / 18)) - ) + for (int x = 0; x < rect.Width; x++) { - UpdateServerTileState(tile, newTile, TileDataType.Tile); + for (int y = 0; y < rect.Height; y++) + { + int realX = rect.X + x; + int realY = rect.Y + y; + + if (realX < 0 || realX >= Main.maxTilesX || realY < 0 || realY >= Main.maxTilesY) + { + return false; + } + } } - } - // Moss and MossBrick are not used in conversion - private static List _convertibleTiles = typeof(TileID.Sets.Conversion) - .GetFields() - .ExceptBy(new[] { nameof(TileID.Sets.Conversion.Moss), nameof(TileID.Sets.Conversion.MossBrick) }, f => f.Name) - .Select(f => (bool[])f.GetValue(null)) - .ToList(); - // PureSand is only used in WorldGen.SpreadDesertWalls, which is server side - private static List _convertibleWalls = typeof(WallID.Sets.Conversion) - .GetFields() - .ExceptBy(new[] { nameof(WallID.Sets.Conversion.PureSand) }, f => f.Name) - .Select(f => (bool[])f.GetValue(null)) - .ToList(); + return true; + } /// - /// Updates a single tile on the server if it is a valid conversion from one tile or wall type to another (eg stone -> corrupt stone) + /// Checks whether the tile rect is at a valid distance to the given player. /// - /// The tile to update - /// The NetTile containing new tile properties - internal void ProcessConversionSpreads(ITile tile, NetTile newTile) + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect at a valid distance, otherwise . + private static bool IsRectDistanceValid(TSPlayer player, TileRect rect) { - var allowTile = false; - if (Main.tileMoss[tile.type] && TileID.Sets.Conversion.Stone[newTile.Type]) - { - allowTile = true; - } - else if ((Main.tileMoss[tile.type] || TileID.Sets.Conversion.Stone[tile.type] || TileID.Sets.Conversion.Ice[tile.type] || TileID.Sets.Conversion.Sandstone[tile.type]) && - (newTile.Type == TileID.Sandstone || newTile.Type == TileID.IceBlock)) - { - // ProjectileID.SandSpray and ProjectileID.SnowSpray - allowTile = true; - } - else + for (int x = 0; x < rect.Width; x++) { - foreach (var tileType in _convertibleTiles) + for (int y = 0; y < rect.Height; y++) { - if (tileType[tile.type] && tileType[newTile.Type]) + int realX = rect.X + x; + int realY = rect.Y + y; + + if (!player.IsInRange(realX, realY)) { - allowTile = true; - break; + return false; } } } - if (allowTile) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect processing a tile conversion update - [{tile.type}] -> [{newTile.Type}]")); - UpdateServerTileState(tile, newTile, TileDataType.Tile); - } + return true; + } - foreach (var wallType in _convertibleWalls) - { - if (wallType[tile.wall] && wallType[newTile.Wall]) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect processing a wall conversion update - [{tile.wall}] -> [{newTile.Wall}]")); - UpdateServerTileState(tile, newTile, TileDataType.Wall); - break; - } - } - } /// - /// Updates a single tile's world state with a set of changes from the networked tile state + /// Checks whether the tile rect is a valid conversion spread (Clentaminator, Powders, etc.). /// - /// The tile to update - /// The NetTile containing the change - /// The type of data to merge into world state - public static void UpdateServerTileState(ITile tile, NetTile newTile, TileDataType updateType) + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches a conversion spread operation, otherwise . + private static bool MatchesConversionSpread(TSPlayer player, TileRect rect) { - //This logic (updateType & TDT.Tile) != 0 is the way Terraria does it (see: Tile.cs/Clear(TileDataType)) - //& is not a typo - we're performing a binary AND test to see if a given flag is set. - - if ((updateType & TileDataType.Tile) != 0) + if (rect.Width != 1 || rect.Height != 1) { - tile.active(newTile.Active); - tile.type = newTile.Type; + return false; + } + + ITile oldTile = Main.tile[rect.X, rect.Y]; + NetTile newTile = rect[0, 0]; + + WorldGenMock.SimulateConversionChange(rect.X, rect.Y, out HashSet validTiles, out HashSet validWalls); - if (newTile.FrameImportant) + if (newTile.Type != oldTile.type && validTiles.Contains(newTile.Type)) + { + if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) { - tile.frameX = newTile.FrameX; - tile.frameY = newTile.FrameY; + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; } - else if (tile.type != newTile.Type || !tile.active()) + else if (!player.HasBuildPermission(rect.X, rect.Y)) { - //This is vanilla logic - if the tile changed types (or wasn't active) the frame values might not be valid - so we reset them to -1. - tile.frameX = -1; - tile.frameY = -1; + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; } - } + else + { + Main.tile[rect.X, rect.Y].type = newTile.Type; + Main.tile[rect.X, rect.Y].frameX = newTile.FrameX; + Main.tile[rect.X, rect.Y].frameY = newTile.FrameY; - if ((updateType & TileDataType.Wall) != 0) - { - tile.wall = newTile.Wall; + return true; + } } - if ((updateType & TileDataType.TilePaint) != 0) + if (newTile.Wall != oldTile.wall && validWalls.Contains(newTile.Wall)) { - tile.color(newTile.TileColor); - tile.fullbrightBlock(newTile.FullbrightBlock); - tile.invisibleBlock(newTile.InvisibleBlock); + // wallbans when? + + if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + else + { + Main.tile[rect.X, rect.Y].wall = newTile.Wall; + + return true; + } } - if ((updateType & TileDataType.WallPaint) != 0) + return false; + } + + + private static readonly Dictionary> PlantToGrassMap = new Dictionary> + { + { TileID.Plants, new HashSet() { - tile.wallColor(newTile.WallColor); - tile.fullbrightWall(newTile.FullbrightWall); - tile.invisibleWall(newTile.InvisibleWall); - } + TileID.Grass, TileID.GolfGrass + } }, + { TileID.HallowedPlants, new HashSet() + { + TileID.HallowedGrass, TileID.GolfGrassHallowed + } }, + { TileID.HallowedPlants2, new HashSet() + { + TileID.HallowedGrass, TileID.GolfGrassHallowed + } }, + { TileID.JunglePlants2, new HashSet() + { + TileID.JungleGrass + } }, + { TileID.AshPlants, new HashSet() + { + TileID.AshGrass + } }, + }; - if ((updateType & TileDataType.Liquid) != 0) + private static readonly Dictionary> GrassToStyleMap = new Dictionary>() + { + { TileID.Plants, new HashSet() { - tile.liquid = newTile.Liquid; - tile.liquidType(newTile.LiquidType); - } + 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 24, 27, 30, 33, 36, 39, 42, + 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, 44, + } }, + { TileID.HallowedPlants, new HashSet() + { + 4, 6, + } }, + { TileID.HallowedPlants2, new HashSet() + { + 2, 3, 4, 6, 7, + } }, + { TileID.JunglePlants2, new HashSet() + { + 9, 10, 11, 12, 13, 14, 15, 16, + } }, + { TileID.AshPlants, new HashSet() + { + 6, 7, 8, 9, 10, + } }, + }; - if ((updateType & TileDataType.Slope) != 0) + /// + /// Checks whether the tile rect is a valid Flower Boots placement. + /// + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches a Flower Boots placement, otherwise . + private static bool MatchesFlowerBoots(TSPlayer player, TileRect rect) + { + if (rect.Width != 1 || rect.Height != 1) { - tile.halfBrick(newTile.IsHalf); - tile.slope(newTile.Slope); + return false; } - if ((updateType & TileDataType.Wiring) != 0) + if (!player.TPlayer.flowerBoots) { - tile.wire(newTile.Wire); - tile.wire2(newTile.Wire2); - tile.wire3(newTile.Wire3); - tile.wire4(newTile.Wire4); + return false; } - if ((updateType & TileDataType.Actuator) != 0) + ITile oldTile = Main.tile[rect.X, rect.Y]; + NetTile newTile = rect[0, 0]; + + if ( + PlantToGrassMap.TryGetValue(newTile.Type, out HashSet grassTiles) && + !oldTile.active() && grassTiles.Contains(Main.tile[rect.X, rect.Y + 1].type) && + GrassToStyleMap[newTile.Type].Contains((ushort)(newTile.FrameX / 18)) + ) { - tile.actuator(newTile.IsActuator); - tile.inActive(newTile.Inactive); + if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + Main.tile[rect.X, rect.Y].active(active: true); + Main.tile[rect.X, rect.Y].type = newTile.Type; + Main.tile[rect.X, rect.Y].frameX = newTile.FrameX; + Main.tile[rect.X, rect.Y].frameY = 0; + + return true; } + + return false; } + + private static readonly Dictionary GrassToMowedMap = new Dictionary + { + { TileID.Grass, TileID.GolfGrass }, + { TileID.HallowedGrass, TileID.GolfGrassHallowed }, + }; + /// - /// Performs on multiple tiles + /// Checks whether the tile rect is a valid grass mow. /// - /// - /// - /// - /// - /// - public static void UpdateMultipleServerTileStates(int x, int y, int width, int height, NetTile[,] newTiles) + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches a grass mowing operation, otherwise . + private static bool MatchesGrassMow(TSPlayer player, TileRect rect) { - for (int i = 0; i < width; i++) + if (rect.Width != 1 || rect.Height != 1) + { + return false; + } + + ITile oldTile = Main.tile[rect.X, rect.Y]; + NetTile newTile = rect[0, 0]; + + if (GrassToMowedMap.TryGetValue(oldTile.type, out ushort mowed) && newTile.Type == mowed) { - for (int j = 0; j < height; j++) + if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + if (!player.HasBuildPermission(rect.X, rect.Y)) + { + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; + } + + Main.tile[rect.X, rect.Y].type = newTile.Type; + if (!newTile.FrameImportant) { - UpdateServerTileState(Main.tile[x + i, y + j], newTiles[i, j], TileDataType.Tile); + Main.tile[rect.X, rect.Y].frameX = -1; + Main.tile[rect.X, rect.Y].frameY = -1; } + + // prevent a common crash when the game checks all vines in an unlimited horizontal length + if (TileID.Sets.IsVine[Main.tile[rect.X, rect.Y + 1].type]) + { + WorldGen.KillTile(rect.X, rect.Y + 1); + } + + return true; } + + return false; } + /// - /// Reads a set of NetTiles from a memory stream + /// Checks whether the tile rect is a valid christmas tree modification. + /// This also required significant reverse engineering effort. /// - /// - /// - /// - /// - static NetTile[,] ReadNetTilesFromStream(System.IO.MemoryStream stream, byte width, byte length) + /// The player the operation originates from. + /// The tile rectangle of the operation. + /// , if the rect matches a christmas tree operation, otherwise . + private static bool MatchesChristmasTree(TSPlayer player, TileRect rect) { - NetTile[,] tiles = new NetTile[width, length]; - for (int x = 0; x < width; x++) + if (rect.Width != 1 || rect.Height != 1) { - for (int y = 0; y < length; y++) + return false; + } + + ITile oldTile = Main.tile[rect.X, rect.Y]; + NetTile newTile = rect[0, 0]; + + if (oldTile.type == TileID.ChristmasTree && newTile.Type == TileID.ChristmasTree) + { + if (newTile.FrameX != 10) + { + return false; + } + + int obj_0 = (newTile.FrameY & 0b0000000000000111); + int obj_1 = (newTile.FrameY & 0b0000000000111000) >> 3; + int obj_2 = (newTile.FrameY & 0b0000001111000000) >> 6; + int obj_3 = (newTile.FrameY & 0b0011110000000000) >> 10; + int obj_x = (newTile.FrameY & 0b1100000000000000) >> 14; + + if (obj_x != 0) + { + return false; + } + + if (obj_0 is < 0 or > 4 || obj_1 is < 0 or > 6 || obj_2 is < 0 or > 11 || obj_3 is < 0 or > 11) + { + return false; + } + + if (!player.HasBuildPermission(rect.X, rect.Y)) { - tiles[x, y] = new NetTile(stream); + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; } + + Main.tile[rect.X, rect.Y].frameY = newTile.FrameY; + + return true; } - return tiles; + return false; } + } + /// + /// This helper class allows simulating a `WorldGen.Convert` call and retrieving all valid changes for a given tile. + /// + internal static class WorldGenMock + { /// - /// Determines whether or not the tile rect should be immediately accepted or rejected + /// This is a mock tile which collects all possible changes the `WorldGen.Convert` code could make in its property setters. /// - /// - /// - static bool ShouldSkipProcessing(GetDataHandlers.SendTileRectEventArgs args) + private sealed class MockTile { - if (args.Player.HasPermission(Permissions.allowclientsideworldedit)) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect accepted clientside world edit from {args.Player.Name}")); - args.Handled = false; - return true; - } + private readonly HashSet _setTypes; + private readonly HashSet _setWalls; + + private ushort _type; + private ushort _wall; - if (args.Width > 4 || args.Length > 4) // as of 1.4.3.6 this is the biggest size the client will send in any case + public MockTile(ushort type, ushort wall, HashSet setTypes, HashSet setWalls) { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from non-vanilla tilemod from {args.Player.Name}")); - return true; + _setTypes = setTypes; + _setWalls = setWalls; + _type = type; + _wall = wall; } - if (args.Player.IsBouncerThrottled()) +#pragma warning disable IDE1006 + + public ushort type { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from throttle from {args.Player.Name}")); - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return true; + get => _type; + set + { + _setTypes.Add(value); + _type = value; + } } - if (args.Player.IsBeingDisabled()) + public ushort wall { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from being disabled from {args.Player.Name}")); - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return true; + get => _wall; + set + { + _setWalls.Add(value); + _wall = value; + } } - return false; +#pragma warning restore IDE1006 } /// - /// Checks if a tile object fits inside the dimensions of a tile rectangle + /// Simulates what would happen if `WorldGen.Convert` was called on the given coordinates and returns two sets with the possible tile type and wall types that the conversion could change the tile to. /// - /// - /// - /// - /// - /// - /// - /// - /// - static bool DoesTileObjectFitInTileRect(int x, int y, int width, int height, short rectWidth, short rectLength, bool[,] processed) + public static void SimulateConversionChange(int x, int y, out HashSet validTiles, out HashSet validWalls) { - if (x + width > rectWidth || y + height > rectLength) - { - // This is ugly, but we want to mark all these tiles as processed so that we're not hitting this check multiple times for one dodgy tile object - for (int i = x; i < rectWidth; i++) - { - for (int j = y; j < rectLength; j++) - { - processed[i, j] = true; - } - } + validTiles = new HashSet(); + validWalls = new HashSet(); - TShock.Log.ConsoleDebug(GetString("Bouncer / SendTileRectHandler - rejected tile object because object dimensions fall outside the tile rect (excessive size)")); - return false; + // all the conversion types used in the code, most apparent in Projectile ai 31 + foreach (int conversionType in new int[] { 0, 1, 2, 3, 4, 5, 6, 7 }) + { + MockTile mock = new(Main.tile[x, y].type, Main.tile[x, y].wall, validTiles, validWalls); + Convert(mock, x, y, conversionType); } - - return true; } - class Debug + /* + * This is a copy of the `WorldGen.Convert` method with the following precise changes: + * - Added a `MockTile tile` parameter + * - Changed the `i` and `j` parameters to `k` and `l` + * - Removed the size parameter + * - Removed the area loop and `Tile tile = Main.tile[k, l]` access in favor of using the tile parameter + * - Removed all calls to `WorldGen.SquareWallFrame`, `NetMessage.SendTileSquare`, `WorldGen.TryKillingTreesAboveIfTheyWouldBecomeInvalid` + * - Changed all `continue` statements to `break` statements + * - Removed the ifs checking the bounds of the tile and wall types + * - Removed branches that would call `WorldGen.KillTile` + * - Changed branches depending on randomness to instead set the property to both values after one another + * + * This overall leads to a method that can be called on a MockTile and real-world coordinates and will spit out the proper conversion changes into the MockTile. + */ + + private static void Convert(MockTile tile, int k, int l, int conversionType) { - /// - /// Displays the difference in IDs between existing tiles and a set of NetTiles to the console - /// - /// X position at the top left of the rect - /// Y position at the top left of the rect - /// Width of the NetTile set - /// Height of the NetTile set - /// New tiles to be visualised - public static void VisualiseTileSetDiff(int tileX, int tileY, int width, int height, NetTile[,] newTiles) + int type = tile.type; + int wall = tile.wall; + switch (conversionType) { - if (TShock.Config.Settings.DebugLogs) - { - char pad = '0'; - for (int y = 0; y < height; y++) + case 4: + if (WallID.Sets.Conversion.Grass[wall] && wall != 81) + { + tile.wall = 81; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall != 83) + { + tile.wall = 83; + } + else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 218) + { + tile.wall = 218; + } + else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 221) + { + tile.wall = 221; + } + else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 192) + { + tile.wall = 192; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 193) + { + tile.wall = 193; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 194) { - int realY = y + tileY; - for (int x = 0; x < width; x++) + tile.wall = 194; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 195) + { + tile.wall = 195; + } + if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type]) && type != 203) + { + tile.type = 203; + } + else if (TileID.Sets.Conversion.JungleGrass[type] && type != 662) + { + tile.type = 662; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 199) + { + tile.type = 199; + } + else if (TileID.Sets.Conversion.Ice[type] && type != 200) + { + tile.type = 200; + } + else if (TileID.Sets.Conversion.Sand[type] && type != 234) + { + tile.type = 234; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 399) + { + tile.type = 399; + } + else if (TileID.Sets.Conversion.Sandstone[type] && type != 401) + { + tile.type = 401; + } + else if (TileID.Sets.Conversion.Thorn[type] && type != 352) + { + tile.type = 352; + } + break; + case 2: + if (WallID.Sets.Conversion.Grass[wall] && wall != 70) + { + tile.wall = 70; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall != 28) + { + tile.wall = 28; + } + else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 219) + { + tile.wall = 219; + } + else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 222) + { + tile.wall = 222; + } + else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 200) + { + tile.wall = 200; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 201) + { + tile.wall = 201; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 202) + { + tile.wall = 202; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 203) + { + tile.wall = 203; + } + if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type]) && type != 117) + { + tile.type = 117; + } + else if (TileID.Sets.Conversion.GolfGrass[type] && type != 492) + { + tile.type = 492; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 109 && type != 492) + { + tile.type = 109; + } + else if (TileID.Sets.Conversion.Ice[type] && type != 164) + { + tile.type = 164; + } + else if (TileID.Sets.Conversion.Sand[type] && type != 116) + { + tile.type = 116; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 402) + { + tile.type = 402; + } + else if (TileID.Sets.Conversion.Sandstone[type] && type != 403) + { + tile.type = 403; + } + if (type == 59 && (Main.tile[k - 1, l].type == 109 || Main.tile[k + 1, l].type == 109 || Main.tile[k, l - 1].type == 109 || Main.tile[k, l + 1].type == 109)) + { + tile.type = 0; + } + break; + case 1: + if (WallID.Sets.Conversion.Grass[wall] && wall != 69) + { + tile.wall = 69; + } + else if (TileID.Sets.Conversion.JungleGrass[type] && type != 661) + { + tile.type = 661; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall != 3) + { + tile.wall = 3; + } + else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 217) + { + tile.wall = 217; + } + else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 220) + { + tile.wall = 220; + } + else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 188) + { + tile.wall = 188; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 189) + { + tile.wall = 189; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 190) + { + tile.wall = 190; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 191) + { + tile.wall = 191; + } + if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type]) && type != 25) + { + tile.type = 25; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 23) + { + tile.type = 23; + } + else if (TileID.Sets.Conversion.Ice[type] && type != 163) + { + tile.type = 163; + } + else if (TileID.Sets.Conversion.Sand[type] && type != 112) + { + tile.type = 112; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 398) + { + tile.type = 398; + } + else if (TileID.Sets.Conversion.Sandstone[type] && type != 400) + { + tile.type = 400; + } + else if (TileID.Sets.Conversion.Thorn[type] && type != 32) + { + tile.type = 32; + } + break; + case 3: + if (WallID.Sets.CanBeConvertedToGlowingMushroom[wall]) + { + tile.wall = 80; + } + if (tile.type == 60) + { + tile.type = 70; + } + break; + case 5: + if ((WallID.Sets.Conversion.Stone[wall] || WallID.Sets.Conversion.NewWall1[wall] || WallID.Sets.Conversion.NewWall2[wall] || WallID.Sets.Conversion.NewWall3[wall] || WallID.Sets.Conversion.NewWall4[wall] || WallID.Sets.Conversion.Ice[wall] || WallID.Sets.Conversion.Sandstone[wall]) && wall != 187) + { + tile.wall = 187; + } + else if ((WallID.Sets.Conversion.HardenedSand[wall] || WallID.Sets.Conversion.Dirt[wall] || WallID.Sets.Conversion.Snow[wall]) && wall != 216) + { + tile.wall = 216; + } + if ((TileID.Sets.Conversion.Grass[type] || TileID.Sets.Conversion.Sand[type] || TileID.Sets.Conversion.Snow[type] || TileID.Sets.Conversion.Dirt[type]) && type != 53) + { + int num = 53; + if (WorldGen.BlockBelowMakesSandConvertIntoHardenedSand(k, l)) { - int realX = x + tileX; - ushort type = Main.tile[realX, realY].type; - string type2 = type.ToString(); - Console.Write((type2.ToString()).PadLeft(3, pad) + (Main.tile[realX, realY].active() ? "a" : "-") + " "); + num = 397; } - Console.Write(" -> "); - for (int x = 0; x < width; x++) + tile.type = (ushort)num; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 397) + { + tile.type = 397; + } + else if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type] || TileID.Sets.Conversion.Ice[type] || TileID.Sets.Conversion.Sandstone[type]) && type != 396) + { + tile.type = 396; + } + break; + case 6: + if ((WallID.Sets.Conversion.Stone[wall] || WallID.Sets.Conversion.NewWall1[wall] || WallID.Sets.Conversion.NewWall2[wall] || WallID.Sets.Conversion.NewWall3[wall] || WallID.Sets.Conversion.NewWall4[wall] || WallID.Sets.Conversion.Ice[wall] || WallID.Sets.Conversion.Sandstone[wall]) && wall != 71) + { + tile.wall = 71; + } + else if ((WallID.Sets.Conversion.HardenedSand[wall] || WallID.Sets.Conversion.Dirt[wall] || WallID.Sets.Conversion.Snow[wall]) && wall != 40) + { + tile.wall = 40; + } + if ((TileID.Sets.Conversion.Grass[type] || TileID.Sets.Conversion.Sand[type] || TileID.Sets.Conversion.HardenedSand[type] || TileID.Sets.Conversion.Snow[type] || TileID.Sets.Conversion.Dirt[type]) && type != 147) + { + tile.type = 147; + } + else if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type] || TileID.Sets.Conversion.Ice[type] || TileID.Sets.Conversion.Sandstone[type]) && type != 161) + { + tile.type = 161; + } + break; + case 7: + if ((WallID.Sets.Conversion.Stone[wall] || WallID.Sets.Conversion.Ice[wall] || WallID.Sets.Conversion.Sandstone[wall]) && wall != 1) + { + tile.wall = 1; + } + else if ((WallID.Sets.Conversion.HardenedSand[wall] || WallID.Sets.Conversion.Snow[wall] || WallID.Sets.Conversion.Dirt[wall]) && wall != 2) + { + tile.wall = 2; + } + else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 196) + { + tile.wall = 196; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 197) + { + tile.wall = 197; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 198) + { + tile.wall = 198; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 199) + { + tile.wall = 199; + } + if ((TileID.Sets.Conversion.Stone[type] || TileID.Sets.Conversion.Ice[type] || TileID.Sets.Conversion.Sandstone[type]) && type != 1) + { + tile.type = 1; + } + else if (TileID.Sets.Conversion.GolfGrass[type] && type != 477) + { + tile.type = 477; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 2 && type != 477) + { + tile.type = 2; + } + else if ((TileID.Sets.Conversion.Sand[type] || TileID.Sets.Conversion.HardenedSand[type] || TileID.Sets.Conversion.Snow[type] || TileID.Sets.Conversion.Dirt[type]) && type != 0) + { + int num2 = 0; + if (WorldGen.TileIsExposedToAir(k, l)) { - int realX = x + tileX; - ushort type = newTiles[x, y].Type; - string type2 = type.ToString(); - Console.Write((type2.ToString()).PadLeft(3, pad) + (newTiles[x, y].Active ? "a" : "-") + " "); + num2 = 2; } - Console.Write("\n"); + tile.type = (ushort)num2; } - } + break; } - - /// - /// Sends a tile rect at the given (tileX, tileY) coordinate, using the given set of NetTiles information to update the tile rect - /// - /// X position at the top left of the rect - /// Y position at the top left of the rect - /// Width of the NetTile set - /// Height of the NetTile set - /// New tiles to place in the rect - /// Player to send the debug display to - public static void DisplayTileSetInGame(short tileX, short tileY, byte width, byte height, NetTile[,] newTiles, TSPlayer player) + if (tile.wall == 69 || tile.wall == 70 || tile.wall == 81) { - for (int x = 0; x < width; x++) + if (l < Main.worldSurface) { - for (int y = 0; y < height; y++) - { - UpdateServerTileState(Main.tile[tileX + x, tileY + y], newTiles[x, y], TileDataType.All); - } - //Add a line of dirt blocks at the bottom for safety - UpdateServerTileState(Main.tile[tileX + x, tileY + height], new NetTile { Active = true, Type = 0 }, TileDataType.All); + tile.wall = 65; + tile.wall = 63; } - - player.SendTileRect(tileX, tileY, width, height); + else + { + tile.wall = 64; + } + } + else if (WallID.Sets.Conversion.Stone[wall] && wall != 1 && wall != 262 && wall != 274 && wall != 61 && wall != 185) + { + tile.wall = 1; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall == 262) + { + tile.wall = 61; + } + else if (WallID.Sets.Conversion.Stone[wall] && wall == 274) + { + tile.wall = 185; + } + if (WallID.Sets.Conversion.NewWall1[wall] && wall != 212) + { + tile.wall = 212; + } + else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 213) + { + tile.wall = 213; + } + else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 214) + { + tile.wall = 214; + } + else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 215) + { + tile.wall = 215; + } + else if (tile.wall == 80) + { + tile.wall = 15; + tile.wall = 64; + } + else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 216) + { + tile.wall = 216; + } + else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 187) + { + tile.wall = 187; + } + if (tile.type == 492) + { + tile.type = 477; + } + else if (TileID.Sets.Conversion.JungleGrass[type] && type != 60) + { + tile.type = 60; + } + else if (TileID.Sets.Conversion.Grass[type] && type != 2 && type != 477) + { + tile.type = 2; + } + else if (TileID.Sets.Conversion.Stone[type] && type != 1) + { + tile.type = 1; + } + else if (TileID.Sets.Conversion.Sand[type] && type != 53) + { + tile.type = 53; + } + else if (TileID.Sets.Conversion.HardenedSand[type] && type != 397) + { + tile.type = 397; + } + else if (TileID.Sets.Conversion.Sandstone[type] && type != 396) + { + tile.type = 396; + } + else if (TileID.Sets.Conversion.Ice[type] && type != 161) + { + tile.type = 161; + } + else if (TileID.Sets.Conversion.MushroomGrass[type]) + { + tile.type = 60; } } } diff --git a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs deleted file mode 100644 index 34f8a4959..000000000 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ /dev/null @@ -1,1330 +0,0 @@ -using System.Collections.Generic; -using System.IO; - -using Terraria; -using Terraria.ID; - -using TShockAPI.Net; - -namespace TShockAPI.Handlers -{ - /// - /// Provides processors for handling tile rect packets. - /// This required many hours of reverse engineering work, and is kindly provided to TShock for free by @punchready. - /// - public sealed class SendTileRectHandlerRefactor : IPacketHandler - { - /// - /// Represents a tile rectangle sent through the packet. - /// - private sealed class TileRect - { - private readonly NetTile[,] _tiles; - public readonly int X; - public readonly int Y; - public readonly int Width; - public readonly int Height; - - /// - /// Accesses the tiles contained in this rect. - /// - /// The X coordinate within the rect. - /// The Y coordinate within the rect. - /// The tile at the given position within the rect. - public NetTile this[int x, int y] => _tiles[x, y]; - - /// - /// Constructs a new tile rect based on the given information. - /// - public TileRect(NetTile[,] tiles, int x, int y, int width, int height) - { - _tiles = tiles; - X = x; - Y = y; - Width = width; - Height = height; - } - - /// - /// Reads a tile rect from the given stream. - /// - /// The resulting tile rect. - public static TileRect Read(MemoryStream stream, int tileX, int tileY, int width, int height) - { - NetTile[,] tiles = new NetTile[width, height]; - for (int x = 0; x < width; x++) - { - for (int y = 0; y < height; y++) - { - tiles[x, y] = new NetTile(); - tiles[x, y].Unpack(stream); // explicit > implicit - } - } - return new TileRect(tiles, tileX, tileY, width, height); - } - } - - /// - /// Represents a common tile rect operation (Placement, State Change, Removal). - /// - private readonly struct TileRectMatch - { - private const short IGNORE_FRAME = -1; - - private enum MatchType - { - Placement, - StateChange, - Removal, - } - - private readonly int Width; - private readonly int Height; - - private readonly ushort TileType; - private readonly short MaxFrameX; - private readonly short MaxFrameY; - private readonly short FrameXStep; - private readonly short FrameYStep; - - private readonly MatchType Type; - - private TileRectMatch(MatchType type, int width, int height, ushort tileType, short maxFrameX, short maxFrameY, short frameXStep, short frameYStep) - { - Type = type; - Width = width; - Height = height; - TileType = tileType; - MaxFrameX = maxFrameX; - MaxFrameY = maxFrameY; - FrameXStep = frameXStep; - FrameYStep = frameYStep; - } - - /// - /// Creates a new placement operation. - /// - /// The width of the placement. - /// The height of the placement. - /// The tile type of the placement. - /// The maximum allowed frameX of the placement. - /// The maximum allowed frameY of the placement. - /// The step size in which frameX changes for this placement, or 1 if any value is allowed. - /// The step size in which frameX changes for this placement, or 1 if any value is allowed. - /// The resulting operation match. - public static TileRectMatch Placement(int width, int height, ushort tileType, short maxFrameX, short maxFrameY, short frameXStep, short frameYStep) - { - return new TileRectMatch(MatchType.Placement, width, height, tileType, maxFrameX, maxFrameY, frameXStep, frameYStep); - } - - /// - /// Creates a new state change operation. - /// - /// The width of the state change. - /// The height of the state change. - /// The target tile type of the state change. - /// The maximum allowed frameX of the state change. - /// The maximum allowed frameY of the state change. - /// The step size in which frameX changes for this placement, or 1 if any value is allowed. - /// The step size in which frameY changes for this placement, or 1 if any value is allowed. - /// The resulting operation match. - public static TileRectMatch StateChange(int width, int height, ushort tileType, short maxFrameX, short maxFrameY, short frameXStep, short frameYStep) - { - return new TileRectMatch(MatchType.StateChange, width, height, tileType, maxFrameX, maxFrameY, frameXStep, frameYStep); - } - - /// - /// Creates a new state change operation which only changes frameX. - /// - /// The width of the state change. - /// The height of the state change. - /// The target tile type of the state change. - /// The maximum allowed frameX of the state change. - /// The step size in which frameX changes for this placement, or 1 if any value is allowed. - /// The resulting operation match. - public static TileRectMatch StateChangeX(int width, int height, ushort tileType, short maxFrame, short frameStep) - { - return new TileRectMatch(MatchType.StateChange, width, height, tileType, maxFrame, IGNORE_FRAME, frameStep, 0); - } - - /// - /// Creates a new state change operation which only changes frameY. - /// - /// The width of the state change. - /// The height of the state change. - /// The target tile type of the state change. - /// The maximum allowed frameY of the state change. - /// The step size in which frameY changes for this placement, or 1 if any value is allowed. - /// The resulting operation match. - public static TileRectMatch StateChangeY(int width, int height, ushort tileType, short maxFrame, short frameStep) - { - return new TileRectMatch(MatchType.StateChange, width, height, tileType, IGNORE_FRAME, maxFrame, 0, frameStep); - } - - /// - /// Creates a new removal operation. - /// - /// The width of the removal. - /// The height of the removal. - /// The target tile type of the removal. - /// The resulting operation match. - public static TileRectMatch Removal(int width, int height, ushort tileType) - { - return new TileRectMatch(MatchType.Removal, width, height, tileType, 0, 0, 0, 0); - } - - /// - /// Determines whether the given tile rectangle matches this operation, and if so, applies it to the world. - /// - /// The player the operation originates from. - /// The tile rectangle of the operation. - /// , if the rect matches this operation and the changes have been applied, otherwise . - public bool Matches(TSPlayer player, TileRect rect) - { - if (rect.Width != Width || rect.Height != Height) - { - return false; - } - - for (int x = 0; x < rect.Width; x++) - { - for (int y = 0; y < rect.Height; y++) - { - NetTile tile = rect[x, y]; - if (Type is MatchType.Placement or MatchType.StateChange) - { - if (tile.Type != TileType) - { - return false; - } - } - if (Type is MatchType.Placement or MatchType.StateChange) - { - if (MaxFrameX != IGNORE_FRAME) - { - if (tile.FrameX < 0 || tile.FrameX > MaxFrameX || tile.FrameX % FrameXStep != 0) - { - return false; - } - } - if (MaxFrameY != IGNORE_FRAME) - { - if (tile.FrameY < 0 || tile.FrameY > MaxFrameY || tile.FrameY % FrameYStep != 0) - { - // this is the only tile type sent in a tile rect where the frame have a different pattern (56, 74, 92 instead of 54, 72, 90) - if (!(TileType == TileID.LunarMonolith && tile.FrameY % FrameYStep == 2)) - { - return false; - } - } - } - } - if (Type == MatchType.Removal) - { - if (tile.Active) - { - return false; - } - } - } - } - - for (int x = rect.X; x < rect.X + rect.Width; x++) - { - for (int y = rect.Y; y < rect.Y + rect.Height; y++) - { - if (!player.HasBuildPermission(x, y)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - } - } - - switch (Type) - { - case MatchType.Placement: - { - return MatchPlacement(player, rect); - } - case MatchType.StateChange: - { - return MatchStateChange(player, rect); - } - case MatchType.Removal: - { - return MatchRemoval(player, rect); - } - } - - return false; - } - - private bool MatchPlacement(TSPlayer player, TileRect rect) - { - for (int x = rect.X; x < rect.Y + rect.Width; x++) - { - for (int y = rect.Y; y < rect.Y + rect.Height; y++) - { - if (Main.tile[x, y].active() && !(Main.tile[x, y].type != TileID.RollingCactus && (Main.tileCut[Main.tile[x, y].type] || TileID.Sets.BreakableWhenPlacing[Main.tile[x, y].type]))) - { - return false; - } - } - } - - // let's hope tile types never go out of short range (they use ushort in terraria's code) - if (TShock.TileBans.TileIsBanned((short)TileType, player)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - - for (int x = 0; x < rect.Width; x++) - { - for (int y = 0; y < rect.Height; y++) - { - Main.tile[x + rect.X, y + rect.Y].active(active: true); - Main.tile[x + rect.X, y + rect.Y].type = rect[x, y].Type; - Main.tile[x + rect.X, y + rect.Y].frameX = rect[x, y].FrameX; - Main.tile[x + rect.X, y + rect.Y].frameY = rect[x, y].FrameY; - } - } - - return true; - } - - private bool MatchStateChange(TSPlayer player, TileRect rect) - { - for (int x = rect.X; x < rect.Y + rect.Width; x++) - { - for (int y = rect.Y; y < rect.Y + rect.Height; y++) - { - if (!Main.tile[x, y].active() || Main.tile[x, y].type != TileType) - { - return false; - } - } - } - - for (int x = 0; x < rect.Width; x++) - { - for (int y = 0; y < rect.Height; y++) - { - if (MaxFrameX != IGNORE_FRAME) - { - Main.tile[x + rect.X, y + rect.Y].frameX = rect[x, y].FrameX; - } - if (MaxFrameY != IGNORE_FRAME) - { - Main.tile[x + rect.X, y + rect.Y].frameY = rect[x, y].FrameY; - } - } - } - - return true; - } - - private bool MatchRemoval(TSPlayer player, TileRect rect) - { - for (int x = rect.X; x < rect.Y + rect.Width; x++) - { - for (int y = rect.Y; y < rect.Y + rect.Height; y++) - { - if (!Main.tile[x, y].active() || Main.tile[x, y].type != TileType) - { - return false; - } - } - } - - for (int x = 0; x < rect.Width; x++) - { - for (int y = 0; y < rect.Height; y++) - { - Main.tile[x + rect.X, y + rect.Y].active(active: false); - Main.tile[x + rect.X, y + rect.Y].frameX = -1; - Main.tile[x + rect.X, y + rect.Y].frameY = -1; - } - } - - return true; - } - } - - /// - /// Contains the complete list of valid tile rect operations the game currently performs. - /// - // The matches restrict the tile rects to only place one kind of tile, and only with the given maximum values and step sizes for frameX and frameY. This performs pretty much perfect checks on the data, allowing only valid placements. - // For TileID.MinecartTrack, the data is taken from `Minecart._trackSwitchOptions`, allowing any framing value in this array (currently 0-36). - // For TileID.Plants, it is taken from `ItemID.Sets.flowerPacketInfo[n].stylesOnPurity`, allowing every style multiplied by 18. - // The other operations are based on code analysis and manual observation. - private static readonly TileRectMatch[] Matches = new TileRectMatch[] - { - TileRectMatch.Placement(2, 3, TileID.TargetDummy, 54, 36, 18, 18), - TileRectMatch.Placement(3, 4, TileID.TeleportationPylon, 468, 54, 18, 18), - TileRectMatch.Placement(2, 3, TileID.DisplayDoll, 126, 36, 18, 18), - TileRectMatch.Placement(2, 3, TileID.HatRack, 90, 54, 18, 18), - TileRectMatch.Placement(2, 2, TileID.ItemFrame, 162, 18, 18, 18), - TileRectMatch.Placement(3, 3, TileID.WeaponsRack2, 90, 36, 18, 18), - TileRectMatch.Placement(1, 1, TileID.FoodPlatter, 18, 0, 18, 18), - TileRectMatch.Placement(1, 1, TileID.LogicSensor, 108, 0, 18, 18), - - TileRectMatch.StateChangeY(3, 2, TileID.Campfire, 54, 18), - TileRectMatch.StateChangeY(4, 3, TileID.Cannon, 468, 18), - TileRectMatch.StateChangeY(2, 2, TileID.ArrowSign, 270, 18), - TileRectMatch.StateChangeY(2, 2, TileID.PaintedArrowSign, 270, 18), - - TileRectMatch.StateChangeX(2, 2, TileID.MusicBoxes, 54, 18), - - TileRectMatch.StateChangeY(2, 3, TileID.LunarMonolith, 92, 18), - TileRectMatch.StateChangeY(2, 3, TileID.BloodMoonMonolith, 90, 18), - TileRectMatch.StateChangeY(2, 3, TileID.VoidMonolith, 90, 18), - TileRectMatch.StateChangeY(2, 3, TileID.EchoMonolith, 90, 18), - TileRectMatch.StateChangeY(2, 3, TileID.ShimmerMonolith, 144, 18), - TileRectMatch.StateChangeY(2, 4, TileID.WaterFountain, 126, 18), - - TileRectMatch.StateChangeX(1, 1, TileID.Candles, 18, 18), - TileRectMatch.StateChangeX(1, 1, TileID.PeaceCandle, 18, 18), - TileRectMatch.StateChangeX(1, 1, TileID.WaterCandle, 18, 18), - TileRectMatch.StateChangeX(1, 1, TileID.PlatinumCandle, 18, 18), - TileRectMatch.StateChangeX(1, 1, TileID.ShadowCandle, 18, 18), - - TileRectMatch.StateChange(1, 1, TileID.Traps, 90, 90, 18, 18), - - TileRectMatch.StateChangeX(1, 1, TileID.WirePipe, 36, 18), - TileRectMatch.StateChangeX(1, 1, TileID.ProjectilePressurePad, 66, 22), - TileRectMatch.StateChangeX(1, 1, TileID.Plants, 792, 18), - TileRectMatch.StateChangeX(1, 1, TileID.MinecartTrack, 36, 1), - - TileRectMatch.Removal(1, 2, TileID.Firework), - TileRectMatch.Removal(1, 1, TileID.LandMine), - }; - - - /// - /// Handles a packet receive event. - /// - public void OnReceive(object sender, GetDataHandlers.SendTileRectEventArgs args) - { - // this permission bypasses all checks for direct access to the world - if (args.Player.HasPermission(Permissions.allowclientsideworldedit)) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect accepted clientside world edit from {args.Player.Name}")); - - // use vanilla handling - args.Handled = false; - return; - } - - // this handler handles the entire logic of this packet - args.Handled = true; - - // as of 1.4 this is the biggest size the client will send in any case, determined by full code analysis - // see default matches above and special cases below - if (args.Width > 4 || args.Length > 4) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from size from {args.Player.Name}")); - - // definitely invalid; do not send any correcting data - return; - } - - // player throttled? - if (args.Player.IsBouncerThrottled()) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from throttle from {args.Player.Name}")); - - // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return; - } - - // player disabled? - if (args.Player.IsBeingDisabled()) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from being disabled from {args.Player.Name}")); - - // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return; - } - - // read the tile rectangle - TileRect rect = TileRect.Read(args.Data, args.TileX, args.TileY, args.Width, args.Length); - - // check if the positioning is valid - if (!IsRectPositionValid(args.Player, rect)) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from out of bounds / build permission from {args.Player.Name}")); - - // send nothing due to out of bounds - return; - } - - // a very special case, due to the clentaminator having a larger range than TSPlayer.IsInRange() allows - if (MatchesConversionSpread(args.Player, rect)) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); - - // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return; - } - - // check if the distance is valid - if (!IsRectDistanceValid(args.Player, rect)) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from out of range from {args.Player.Name}")); - - // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return; - } - - // a very special case, due to the flower seed check otherwise hijacking this - if (MatchesFlowerBoots(args.Player, rect)) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); - - // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return; - } - - // check if the rect matches any valid operation - foreach (TileRectMatch match in Matches) - { - if (match.Matches(args.Player, rect)) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); - - // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return; - } - } - - // a few special cases - if ( - MatchesConversionSpread(args.Player, rect) || - MatchesGrassMow(args.Player, rect) || - MatchesChristmasTree(args.Player, rect) - ) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); - - // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return; - } - - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from matches from {args.Player.Name}")); - - // send correcting data - args.Player.SendTileRect(args.TileX, args.TileY, args.Length, args.Width); - return; - } - - /// - /// Checks whether the tile rect is at a valid position for the given player. - /// - /// The player the operation originates from. - /// The tile rectangle of the operation. - /// , if the rect at a valid position, otherwise . - private static bool IsRectPositionValid(TSPlayer player, TileRect rect) - { - for (int x = 0; x < rect.Width; x++) - { - for (int y = 0; y < rect.Height; y++) - { - int realX = rect.X + x; - int realY = rect.Y + y; - - if (realX < 0 || realX >= Main.maxTilesX || realY < 0 || realY >= Main.maxTilesY) - { - return false; - } - } - } - - return true; - } - - /// - /// Checks whether the tile rect is at a valid distance to the given player. - /// - /// The player the operation originates from. - /// The tile rectangle of the operation. - /// , if the rect at a valid distance, otherwise . - private static bool IsRectDistanceValid(TSPlayer player, TileRect rect) - { - for (int x = 0; x < rect.Width; x++) - { - for (int y = 0; y < rect.Height; y++) - { - int realX = rect.X + x; - int realY = rect.Y + y; - - if (!player.IsInRange(realX, realY)) - { - return false; - } - } - } - - return true; - } - - - /// - /// Checks whether the tile rect is a valid conversion spread (Clentaminator, Powders, etc.). - /// - /// The player the operation originates from. - /// The tile rectangle of the operation. - /// , if the rect matches a conversion spread operation, otherwise . - private static bool MatchesConversionSpread(TSPlayer player, TileRect rect) - { - if (rect.Width != 1 || rect.Height != 1) - { - return false; - } - - ITile oldTile = Main.tile[rect.X, rect.Y]; - NetTile newTile = rect[0, 0]; - - WorldGenMock.SimulateConversionChange(rect.X, rect.Y, out HashSet validTiles, out HashSet validWalls); - - if (newTile.Type != oldTile.type && validTiles.Contains(newTile.Type)) - { - if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - else if (!player.HasBuildPermission(rect.X, rect.Y)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - else - { - Main.tile[rect.X, rect.Y].type = newTile.Type; - Main.tile[rect.X, rect.Y].frameX = newTile.FrameX; - Main.tile[rect.X, rect.Y].frameY = newTile.FrameY; - - return true; - } - } - - if (newTile.Wall != oldTile.wall && validWalls.Contains(newTile.Wall)) - { - // wallbans when? - - if (!player.HasBuildPermission(rect.X, rect.Y)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - else - { - Main.tile[rect.X, rect.Y].wall = newTile.Wall; - - return true; - } - } - - return false; - } - - - private static readonly Dictionary> PlantToGrassMap = new Dictionary> - { - { TileID.Plants, new HashSet() - { - TileID.Grass, TileID.GolfGrass - } }, - { TileID.HallowedPlants, new HashSet() - { - TileID.HallowedGrass, TileID.GolfGrassHallowed - } }, - { TileID.HallowedPlants2, new HashSet() - { - TileID.HallowedGrass, TileID.GolfGrassHallowed - } }, - { TileID.JunglePlants2, new HashSet() - { - TileID.JungleGrass - } }, - { TileID.AshPlants, new HashSet() - { - TileID.AshGrass - } }, - }; - - private static readonly Dictionary> GrassToStyleMap = new Dictionary>() - { - { TileID.Plants, new HashSet() - { - 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 24, 27, 30, 33, 36, 39, 42, - 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, 44, - } }, - { TileID.HallowedPlants, new HashSet() - { - 4, 6, - } }, - { TileID.HallowedPlants2, new HashSet() - { - 2, 3, 4, 6, 7, - } }, - { TileID.JunglePlants2, new HashSet() - { - 9, 10, 11, 12, 13, 14, 15, 16, - } }, - { TileID.AshPlants, new HashSet() - { - 6, 7, 8, 9, 10, - } }, - }; - - /// - /// Checks whether the tile rect is a valid Flower Boots placement. - /// - /// The player the operation originates from. - /// The tile rectangle of the operation. - /// , if the rect matches a Flower Boots placement, otherwise . - private static bool MatchesFlowerBoots(TSPlayer player, TileRect rect) - { - if (rect.Width != 1 || rect.Height != 1) - { - return false; - } - - if (!player.TPlayer.flowerBoots) - { - return false; - } - - ITile oldTile = Main.tile[rect.X, rect.Y]; - NetTile newTile = rect[0, 0]; - - if ( - PlantToGrassMap.TryGetValue(newTile.Type, out HashSet grassTiles) && - !oldTile.active() && grassTiles.Contains(Main.tile[rect.X, rect.Y + 1].type) && - GrassToStyleMap[newTile.Type].Contains((ushort)(newTile.FrameX / 18)) - ) - { - if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - - if (!player.HasBuildPermission(rect.X, rect.Y)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - - Main.tile[rect.X, rect.Y].active(active: true); - Main.tile[rect.X, rect.Y].type = newTile.Type; - Main.tile[rect.X, rect.Y].frameX = newTile.FrameX; - Main.tile[rect.X, rect.Y].frameY = 0; - - return true; - } - - return false; - } - - - private static readonly Dictionary GrassToMowedMap = new Dictionary - { - { TileID.Grass, TileID.GolfGrass }, - { TileID.HallowedGrass, TileID.GolfGrassHallowed }, - }; - - /// - /// Checks whether the tile rect is a valid grass mow. - /// - /// The player the operation originates from. - /// The tile rectangle of the operation. - /// , if the rect matches a grass mowing operation, otherwise . - private static bool MatchesGrassMow(TSPlayer player, TileRect rect) - { - if (rect.Width != 1 || rect.Height != 1) - { - return false; - } - - ITile oldTile = Main.tile[rect.X, rect.Y]; - NetTile newTile = rect[0, 0]; - - if (GrassToMowedMap.TryGetValue(oldTile.type, out ushort mowed) && newTile.Type == mowed) - { - if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - - if (!player.HasBuildPermission(rect.X, rect.Y)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - - Main.tile[rect.X, rect.Y].type = newTile.Type; - if (!newTile.FrameImportant) - { - Main.tile[rect.X, rect.Y].frameX = -1; - Main.tile[rect.X, rect.Y].frameY = -1; - } - - // prevent a common crash when the game checks all vines in an unlimited horizontal length - if (TileID.Sets.IsVine[Main.tile[rect.X, rect.Y + 1].type]) - { - WorldGen.KillTile(rect.X, rect.Y + 1); - } - - return true; - } - - return false; - } - - - /// - /// Checks whether the tile rect is a valid christmas tree modification. - /// This also required significant reverse engineering effort. - /// - /// The player the operation originates from. - /// The tile rectangle of the operation. - /// , if the rect matches a christmas tree operation, otherwise . - private static bool MatchesChristmasTree(TSPlayer player, TileRect rect) - { - if (rect.Width != 1 || rect.Height != 1) - { - return false; - } - - ITile oldTile = Main.tile[rect.X, rect.Y]; - NetTile newTile = rect[0, 0]; - - if (oldTile.type == TileID.ChristmasTree && newTile.Type == TileID.ChristmasTree) - { - if (newTile.FrameX != 10) - { - return false; - } - - int obj_0 = (newTile.FrameY & 0b0000000000000111); - int obj_1 = (newTile.FrameY & 0b0000000000111000) >> 3; - int obj_2 = (newTile.FrameY & 0b0000001111000000) >> 6; - int obj_3 = (newTile.FrameY & 0b0011110000000000) >> 10; - int obj_x = (newTile.FrameY & 0b1100000000000000) >> 14; - - if (obj_x != 0) - { - return false; - } - - if (obj_0 is < 0 or > 4 || obj_1 is < 0 or > 6 || obj_2 is < 0 or > 11 || obj_3 is < 0 or > 11) - { - return false; - } - - if (!player.HasBuildPermission(rect.X, rect.Y)) - { - // for simplicity, let's pretend that the edit was valid, but do not execute it - return true; - } - - Main.tile[rect.X, rect.Y].frameY = newTile.FrameY; - - return true; - } - - return false; - } - } - - /// - /// This helper class allows simulating a `WorldGen.Convert` call and retrieving all valid changes for a given tile. - /// - internal static class WorldGenMock - { - /// - /// This is a mock tile which collects all possible changes the `WorldGen.Convert` code could make in its property setters. - /// - private sealed class MockTile - { - private readonly HashSet _setTypes; - private readonly HashSet _setWalls; - - private ushort _type; - private ushort _wall; - - public MockTile(ushort type, ushort wall, HashSet setTypes, HashSet setWalls) - { - _setTypes = setTypes; - _setWalls = setWalls; - _type = type; - _wall = wall; - } - -#pragma warning disable IDE1006 - - public ushort type - { - get => _type; - set - { - _setTypes.Add(value); - _type = value; - } - } - - public ushort wall - { - get => _wall; - set - { - _setWalls.Add(value); - _wall = value; - } - } - -#pragma warning restore IDE1006 - } - - /// - /// Simulates what would happen if `WorldGen.Convert` was called on the given coordinates and returns two sets with the possible tile type and wall types that the conversion could change the tile to. - /// - public static void SimulateConversionChange(int x, int y, out HashSet validTiles, out HashSet validWalls) - { - validTiles = new HashSet(); - validWalls = new HashSet(); - - // all the conversion types used in the code, most apparent in Projectile ai 31 - foreach (int conversionType in new int[] { 0, 1, 2, 3, 4, 5, 6, 7 }) - { - MockTile mock = new(Main.tile[x, y].type, Main.tile[x, y].wall, validTiles, validWalls); - Convert(mock, x, y, conversionType); - } - } - - /* - * This is a copy of the `WorldGen.Convert` method with the following precise changes: - * - Added a `MockTile tile` parameter - * - Changed the `i` and `j` parameters to `k` and `l` - * - Removed the size parameter - * - Removed the area loop and `Tile tile = Main.tile[k, l]` access in favor of using the tile parameter - * - Removed all calls to `WorldGen.SquareWallFrame`, `NetMessage.SendTileSquare`, `WorldGen.TryKillingTreesAboveIfTheyWouldBecomeInvalid` - * - Changed all `continue` statements to `break` statements - * - Removed the ifs checking the bounds of the tile and wall types - * - Removed branches that would call `WorldGen.KillTile` - * - Changed branches depending on randomness to instead set the property to both values after one another - * - * This overall leads to a method that can be called on a MockTile and real-world coordinates and will spit out the proper conversion changes into the MockTile. - */ - - private static void Convert(MockTile tile, int k, int l, int conversionType) - { - int type = tile.type; - int wall = tile.wall; - switch (conversionType) - { - case 4: - if (WallID.Sets.Conversion.Grass[wall] && wall != 81) - { - tile.wall = 81; - } - else if (WallID.Sets.Conversion.Stone[wall] && wall != 83) - { - tile.wall = 83; - } - else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 218) - { - tile.wall = 218; - } - else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 221) - { - tile.wall = 221; - } - else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 192) - { - tile.wall = 192; - } - else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 193) - { - tile.wall = 193; - } - else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 194) - { - tile.wall = 194; - } - else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 195) - { - tile.wall = 195; - } - if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type]) && type != 203) - { - tile.type = 203; - } - else if (TileID.Sets.Conversion.JungleGrass[type] && type != 662) - { - tile.type = 662; - } - else if (TileID.Sets.Conversion.Grass[type] && type != 199) - { - tile.type = 199; - } - else if (TileID.Sets.Conversion.Ice[type] && type != 200) - { - tile.type = 200; - } - else if (TileID.Sets.Conversion.Sand[type] && type != 234) - { - tile.type = 234; - } - else if (TileID.Sets.Conversion.HardenedSand[type] && type != 399) - { - tile.type = 399; - } - else if (TileID.Sets.Conversion.Sandstone[type] && type != 401) - { - tile.type = 401; - } - else if (TileID.Sets.Conversion.Thorn[type] && type != 352) - { - tile.type = 352; - } - break; - case 2: - if (WallID.Sets.Conversion.Grass[wall] && wall != 70) - { - tile.wall = 70; - } - else if (WallID.Sets.Conversion.Stone[wall] && wall != 28) - { - tile.wall = 28; - } - else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 219) - { - tile.wall = 219; - } - else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 222) - { - tile.wall = 222; - } - else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 200) - { - tile.wall = 200; - } - else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 201) - { - tile.wall = 201; - } - else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 202) - { - tile.wall = 202; - } - else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 203) - { - tile.wall = 203; - } - if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type]) && type != 117) - { - tile.type = 117; - } - else if (TileID.Sets.Conversion.GolfGrass[type] && type != 492) - { - tile.type = 492; - } - else if (TileID.Sets.Conversion.Grass[type] && type != 109 && type != 492) - { - tile.type = 109; - } - else if (TileID.Sets.Conversion.Ice[type] && type != 164) - { - tile.type = 164; - } - else if (TileID.Sets.Conversion.Sand[type] && type != 116) - { - tile.type = 116; - } - else if (TileID.Sets.Conversion.HardenedSand[type] && type != 402) - { - tile.type = 402; - } - else if (TileID.Sets.Conversion.Sandstone[type] && type != 403) - { - tile.type = 403; - } - if (type == 59 && (Main.tile[k - 1, l].type == 109 || Main.tile[k + 1, l].type == 109 || Main.tile[k, l - 1].type == 109 || Main.tile[k, l + 1].type == 109)) - { - tile.type = 0; - } - break; - case 1: - if (WallID.Sets.Conversion.Grass[wall] && wall != 69) - { - tile.wall = 69; - } - else if (TileID.Sets.Conversion.JungleGrass[type] && type != 661) - { - tile.type = 661; - } - else if (WallID.Sets.Conversion.Stone[wall] && wall != 3) - { - tile.wall = 3; - } - else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 217) - { - tile.wall = 217; - } - else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 220) - { - tile.wall = 220; - } - else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 188) - { - tile.wall = 188; - } - else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 189) - { - tile.wall = 189; - } - else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 190) - { - tile.wall = 190; - } - else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 191) - { - tile.wall = 191; - } - if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type]) && type != 25) - { - tile.type = 25; - } - else if (TileID.Sets.Conversion.Grass[type] && type != 23) - { - tile.type = 23; - } - else if (TileID.Sets.Conversion.Ice[type] && type != 163) - { - tile.type = 163; - } - else if (TileID.Sets.Conversion.Sand[type] && type != 112) - { - tile.type = 112; - } - else if (TileID.Sets.Conversion.HardenedSand[type] && type != 398) - { - tile.type = 398; - } - else if (TileID.Sets.Conversion.Sandstone[type] && type != 400) - { - tile.type = 400; - } - else if (TileID.Sets.Conversion.Thorn[type] && type != 32) - { - tile.type = 32; - } - break; - case 3: - if (WallID.Sets.CanBeConvertedToGlowingMushroom[wall]) - { - tile.wall = 80; - } - if (tile.type == 60) - { - tile.type = 70; - } - break; - case 5: - if ((WallID.Sets.Conversion.Stone[wall] || WallID.Sets.Conversion.NewWall1[wall] || WallID.Sets.Conversion.NewWall2[wall] || WallID.Sets.Conversion.NewWall3[wall] || WallID.Sets.Conversion.NewWall4[wall] || WallID.Sets.Conversion.Ice[wall] || WallID.Sets.Conversion.Sandstone[wall]) && wall != 187) - { - tile.wall = 187; - } - else if ((WallID.Sets.Conversion.HardenedSand[wall] || WallID.Sets.Conversion.Dirt[wall] || WallID.Sets.Conversion.Snow[wall]) && wall != 216) - { - tile.wall = 216; - } - if ((TileID.Sets.Conversion.Grass[type] || TileID.Sets.Conversion.Sand[type] || TileID.Sets.Conversion.Snow[type] || TileID.Sets.Conversion.Dirt[type]) && type != 53) - { - int num = 53; - if (WorldGen.BlockBelowMakesSandConvertIntoHardenedSand(k, l)) - { - num = 397; - } - tile.type = (ushort)num; - } - else if (TileID.Sets.Conversion.HardenedSand[type] && type != 397) - { - tile.type = 397; - } - else if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type] || TileID.Sets.Conversion.Ice[type] || TileID.Sets.Conversion.Sandstone[type]) && type != 396) - { - tile.type = 396; - } - break; - case 6: - if ((WallID.Sets.Conversion.Stone[wall] || WallID.Sets.Conversion.NewWall1[wall] || WallID.Sets.Conversion.NewWall2[wall] || WallID.Sets.Conversion.NewWall3[wall] || WallID.Sets.Conversion.NewWall4[wall] || WallID.Sets.Conversion.Ice[wall] || WallID.Sets.Conversion.Sandstone[wall]) && wall != 71) - { - tile.wall = 71; - } - else if ((WallID.Sets.Conversion.HardenedSand[wall] || WallID.Sets.Conversion.Dirt[wall] || WallID.Sets.Conversion.Snow[wall]) && wall != 40) - { - tile.wall = 40; - } - if ((TileID.Sets.Conversion.Grass[type] || TileID.Sets.Conversion.Sand[type] || TileID.Sets.Conversion.HardenedSand[type] || TileID.Sets.Conversion.Snow[type] || TileID.Sets.Conversion.Dirt[type]) && type != 147) - { - tile.type = 147; - } - else if ((Main.tileMoss[type] || TileID.Sets.Conversion.Stone[type] || TileID.Sets.Conversion.Ice[type] || TileID.Sets.Conversion.Sandstone[type]) && type != 161) - { - tile.type = 161; - } - break; - case 7: - if ((WallID.Sets.Conversion.Stone[wall] || WallID.Sets.Conversion.Ice[wall] || WallID.Sets.Conversion.Sandstone[wall]) && wall != 1) - { - tile.wall = 1; - } - else if ((WallID.Sets.Conversion.HardenedSand[wall] || WallID.Sets.Conversion.Snow[wall] || WallID.Sets.Conversion.Dirt[wall]) && wall != 2) - { - tile.wall = 2; - } - else if (WallID.Sets.Conversion.NewWall1[wall] && wall != 196) - { - tile.wall = 196; - } - else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 197) - { - tile.wall = 197; - } - else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 198) - { - tile.wall = 198; - } - else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 199) - { - tile.wall = 199; - } - if ((TileID.Sets.Conversion.Stone[type] || TileID.Sets.Conversion.Ice[type] || TileID.Sets.Conversion.Sandstone[type]) && type != 1) - { - tile.type = 1; - } - else if (TileID.Sets.Conversion.GolfGrass[type] && type != 477) - { - tile.type = 477; - } - else if (TileID.Sets.Conversion.Grass[type] && type != 2 && type != 477) - { - tile.type = 2; - } - else if ((TileID.Sets.Conversion.Sand[type] || TileID.Sets.Conversion.HardenedSand[type] || TileID.Sets.Conversion.Snow[type] || TileID.Sets.Conversion.Dirt[type]) && type != 0) - { - int num2 = 0; - if (WorldGen.TileIsExposedToAir(k, l)) - { - num2 = 2; - } - tile.type = (ushort)num2; - } - break; - } - if (tile.wall == 69 || tile.wall == 70 || tile.wall == 81) - { - if (l < Main.worldSurface) - { - tile.wall = 65; - tile.wall = 63; - } - else - { - tile.wall = 64; - } - } - else if (WallID.Sets.Conversion.Stone[wall] && wall != 1 && wall != 262 && wall != 274 && wall != 61 && wall != 185) - { - tile.wall = 1; - } - else if (WallID.Sets.Conversion.Stone[wall] && wall == 262) - { - tile.wall = 61; - } - else if (WallID.Sets.Conversion.Stone[wall] && wall == 274) - { - tile.wall = 185; - } - if (WallID.Sets.Conversion.NewWall1[wall] && wall != 212) - { - tile.wall = 212; - } - else if (WallID.Sets.Conversion.NewWall2[wall] && wall != 213) - { - tile.wall = 213; - } - else if (WallID.Sets.Conversion.NewWall3[wall] && wall != 214) - { - tile.wall = 214; - } - else if (WallID.Sets.Conversion.NewWall4[wall] && wall != 215) - { - tile.wall = 215; - } - else if (tile.wall == 80) - { - tile.wall = 15; - tile.wall = 64; - } - else if (WallID.Sets.Conversion.HardenedSand[wall] && wall != 216) - { - tile.wall = 216; - } - else if (WallID.Sets.Conversion.Sandstone[wall] && wall != 187) - { - tile.wall = 187; - } - if (tile.type == 492) - { - tile.type = 477; - } - else if (TileID.Sets.Conversion.JungleGrass[type] && type != 60) - { - tile.type = 60; - } - else if (TileID.Sets.Conversion.Grass[type] && type != 2 && type != 477) - { - tile.type = 2; - } - else if (TileID.Sets.Conversion.Stone[type] && type != 1) - { - tile.type = 1; - } - else if (TileID.Sets.Conversion.Sand[type] && type != 53) - { - tile.type = 53; - } - else if (TileID.Sets.Conversion.HardenedSand[type] && type != 397) - { - tile.type = 397; - } - else if (TileID.Sets.Conversion.Sandstone[type] && type != 396) - { - tile.type = 396; - } - else if (TileID.Sets.Conversion.Ice[type] && type != 161) - { - tile.type = 161; - } - else if (TileID.Sets.Conversion.MushroomGrass[type]) - { - tile.type = 60; - } - } - } -} From 3d11d84d7339ffe7f165cf2f15316a1d6660dd48 Mon Sep 17 00:00:00 2001 From: SGKoishi Date: Sat, 13 May 2023 03:56:07 -0700 Subject: [PATCH 09/27] Fix dump-reference-data mutate command name --- TShockAPI/Permissions.cs | 15 +++------------ docs/changelog.md | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/TShockAPI/Permissions.cs b/TShockAPI/Permissions.cs index d7ec7166d..debc8ea59 100644 --- a/TShockAPI/Permissions.cs +++ b/TShockAPI/Permissions.cs @@ -529,18 +529,9 @@ public static void DumpDescriptions() field.GetCustomAttributes(false).FirstOrDefault(o => o is DescriptionAttribute) as DescriptionAttribute; var desc = descattr != null && !string.IsNullOrWhiteSpace(descattr.Description) ? descattr.Description : GetString("No description available."); - var commands = GetCommands(name); - foreach (var c in commands) - { - for (var i = 0; i < c.Names.Count; i++) - { - c.Names[i] = "/" + c.Names[i]; - } - } - var strs = - commands.Select( - c => - c.Name + (c.Names.Count > 1 ? " ({0})".SFormat(string.Join(" ", c.Names.ToArray(), 1, c.Names.Count - 1)) : "")); + var strs = GetCommands(name).Select(c => c.Names.Count > 1 + ? $"{c.Name} ({string.Join(" ", c.Names.Skip(1).Select(n => $"/{n}"))})" + : c.Name); sb.AppendLine($"## {name}"); sb.AppendLine($"{desc}"); diff --git a/docs/changelog.md b/docs/changelog.md index 3f64f3554..d7b1d19f3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -78,7 +78,7 @@ Use past tense when adding new entries; sign your name off when you add or chang * If there is no section called "Upcoming changes" below this line, please add one with `## Upcoming changes` as the first line, and then a bulleted item directly after with the first change. --> ## Upcoming changes -Your changes could be here! +* Fixed `/dump-reference-data` mutate the command names. (#2943, @sgkoishi) ## TShock 5.2 * An additional option `pvpwithnoteam` is added at `PvPMode` to enable PVP with no team. (@CelestialAnarchy, #2617, @ATFGK) From 51ecef73a16dd1df10bf093ef529be57c5bd1b64 Mon Sep 17 00:00:00 2001 From: Stargazing Koishi Date: Sat, 13 May 2023 04:09:15 -0700 Subject: [PATCH 10/27] Optimize Linq Co-authored-by: Arthri <41360489+Arthri@users.noreply.github.com> --- TShockAPI/Permissions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TShockAPI/Permissions.cs b/TShockAPI/Permissions.cs index debc8ea59..99ac5d7d2 100644 --- a/TShockAPI/Permissions.cs +++ b/TShockAPI/Permissions.cs @@ -530,8 +530,8 @@ public static void DumpDescriptions() var desc = descattr != null && !string.IsNullOrWhiteSpace(descattr.Description) ? descattr.Description : GetString("No description available."); var strs = GetCommands(name).Select(c => c.Names.Count > 1 - ? $"{c.Name} ({string.Join(" ", c.Names.Skip(1).Select(n => $"/{n}"))})" - : c.Name); + ? $"/{c.Name} (/{string.Join(" /", c.Names.Skip(1))})" + : $"/{c.Name}"); sb.AppendLine($"## {name}"); sb.AppendLine($"{desc}"); From 07bf66f072aef44c43559ac424ecb3990c4d4beb Mon Sep 17 00:00:00 2001 From: punchready <22683812+punchready@users.noreply.github.com> Date: Tue, 6 Jun 2023 13:25:03 +0200 Subject: [PATCH 11/27] Fix MatchPlacement allowing auto breakable tiles --- TShockAPI/Handlers/SendTileRectHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TShockAPI/Handlers/SendTileRectHandler.cs b/TShockAPI/Handlers/SendTileRectHandler.cs index 64d9d8a79..3aa4b14ce 100644 --- a/TShockAPI/Handlers/SendTileRectHandler.cs +++ b/TShockAPI/Handlers/SendTileRectHandler.cs @@ -266,7 +266,7 @@ private bool MatchPlacement(TSPlayer player, TileRect rect) { for (int y = rect.Y; y < rect.Y + rect.Height; y++) { - if (Main.tile[x, y].active() && !(Main.tile[x, y].type != TileID.RollingCactus && (Main.tileCut[Main.tile[x, y].type] || TileID.Sets.BreakableWhenPlacing[Main.tile[x, y].type]))) + if (Main.tile[x, y].active()) // the client will kill tiles that auto break before placing the object { return false; } From 149ca8a70cdc7b2994ed1584c25230ff7378b0ba Mon Sep 17 00:00:00 2001 From: punchready <22683812+punchready@users.noreply.github.com> Date: Wed, 7 Jun 2023 03:05:10 +0200 Subject: [PATCH 12/27] Remove duplicate conversion spread matching --- TShockAPI/Handlers/SendTileRectHandler.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/TShockAPI/Handlers/SendTileRectHandler.cs b/TShockAPI/Handlers/SendTileRectHandler.cs index 3aa4b14ce..d85185b86 100644 --- a/TShockAPI/Handlers/SendTileRectHandler.cs +++ b/TShockAPI/Handlers/SendTileRectHandler.cs @@ -506,11 +506,7 @@ public void OnReceive(object sender, GetDataHandlers.SendTileRectEventArgs args) } // a few special cases - if ( - MatchesConversionSpread(args.Player, rect) || - MatchesGrassMow(args.Player, rect) || - MatchesChristmasTree(args.Player, rect) - ) + if (MatchesGrassMow(args.Player, rect) || MatchesChristmasTree(args.Player, rect)) { TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from {args.Player.Name}")); From b1a054472fcabd14182c9dd1ec7fa3871ab9eb39 Mon Sep 17 00:00:00 2001 From: Cardinal System Date: Tue, 15 Aug 2023 09:53:09 +0000 Subject: [PATCH 13/27] =?UTF-8?q?Update=20translation=20template=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/template.pot | 592 +++++++++++++++++++++++----------------------- 1 file changed, 292 insertions(+), 300 deletions(-) diff --git a/i18n/template.pot b/i18n/template.pot index a4fe57ab9..9da0b2e07 100644 --- a/i18n/template.pot +++ b/i18n/template.pot @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: TShock\n" -"POT-Creation-Date: 2022-12-06 05:43:49+0000\n" -"PO-Revision-Date: 2022-12-06 05:43:50+0000\n" +"POT-Creation-Date: 2023-08-15 09:53:07+0000\n" +"PO-Revision-Date: 2023-08-15 09:53:09+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -10,8 +10,8 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: GetText.NET Extractor\n" -#: ../../TShockAPI/DB/CharacterManager.cs:194 -#: ../../TShockAPI/DB/CharacterManager.cs:264 +#: ../../TShockAPI/DB/CharacterManager.cs:197 +#: ../../TShockAPI/DB/CharacterManager.cs:267 #, csharp-format msgctxt "{0} is a player name" msgid "Skipping SSC save (due to tshock.ignore.ssc) for {0}" @@ -161,31 +161,31 @@ msgstr "" msgid "- Palm trees :" msgstr "" -#: ../../TShockAPI/TShock.cs:963 +#: ../../TShockAPI/TShock.cs:999 msgid "" "!!! > Set DisableLoginBeforeJoin to true in the config file and /reload if " "this is a problem." msgstr "" -#: ../../TShockAPI/TShock.cs:957 +#: ../../TShockAPI/TShock.cs:993 msgid "" "!!! > Set DisableUUIDLogin to true in the config file and /reload if this is " "a problem." msgstr "" -#: ../../TShockAPI/TShock.cs:962 +#: ../../TShockAPI/TShock.cs:998 msgid "" "!!! Login before join is enabled. Existing accounts can login & the server " "password will be bypassed." msgstr "" -#: ../../TShockAPI/TShock.cs:951 +#: ../../TShockAPI/TShock.cs:987 msgid "" "!!! The server password in config.json was overridden by the interactive " "prompt and will be ignored." msgstr "" -#: ../../TShockAPI/TShock.cs:956 +#: ../../TShockAPI/TShock.cs:992 msgid "" "!!! UUID login is enabled. If a user's UUID matches an account, the server " "password will be bypassed." @@ -249,12 +249,12 @@ msgstr "" msgid "{0} ({1}) failed to change the password for account {2}." msgstr "" -#: ../../TShockAPI/TShock.cs:1659 +#: ../../TShockAPI/TShock.cs:1695 #, csharp-format msgid "{0} ({1}) from '{2}' group from '{3}' joined. ({4}/{5})" msgstr "" -#: ../../TShockAPI/TShock.cs:1667 +#: ../../TShockAPI/TShock.cs:1703 #, csharp-format msgid "{0} ({1}) from '{2}' group joined. ({3}/{4})" msgstr "" @@ -266,7 +266,7 @@ msgid "" "automatically." msgstr "" -#: ../../TShockAPI/TShock.cs:1663 +#: ../../TShockAPI/TShock.cs:1699 #, csharp-format msgid "{0} ({1}) has joined." msgstr "" @@ -367,17 +367,17 @@ msgstr "" msgid "{0} added account {1} to group {2}." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3546 +#: ../../TShockAPI/GetDataHandlers.cs:3587 #, csharp-format msgid "{0} applied advanced combat techniques volume 2!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3564 +#: ../../TShockAPI/GetDataHandlers.cs:3605 #, csharp-format msgid "{0} applied advanced combat techniques!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3543 +#: ../../TShockAPI/GetDataHandlers.cs:3584 #, csharp-format msgid "{0} applied traveling merchant's satchel!" msgstr "" @@ -387,8 +387,8 @@ msgstr "" msgid "{0} attempted to register for the account {1} but it was already taken." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2637 -#: ../../TShockAPI/GetDataHandlers.cs:3212 +#: ../../TShockAPI/GetDataHandlers.cs:2672 +#: ../../TShockAPI/GetDataHandlers.cs:3252 #, csharp-format msgid "{0} authenticated successfully as user {1}." msgstr "" @@ -398,7 +398,7 @@ msgstr "" msgid "{0} authenticated successfully as user: {1}." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1955 +#: ../../TShockAPI/TSPlayer.cs:2042 #, csharp-format msgid "{0} banned {1} for '{2}'." msgstr "" @@ -486,7 +486,7 @@ msgstr "" msgid "{0} disabled xmas mode." msgstr "" -#: ../../TShockAPI/TShock.cs:1398 +#: ../../TShockAPI/TShock.cs:1434 #, csharp-format msgid "{0} disconnected." msgstr "" @@ -590,12 +590,12 @@ msgstr "" msgid "{0} has ended the Old One's Army event." msgstr "" -#: ../../TShockAPI/TShock.cs:1670 +#: ../../TShockAPI/TShock.cs:1706 #, csharp-format msgid "{0} has joined." msgstr "" -#: ../../TShockAPI/TShock.cs:1674 +#: ../../TShockAPI/TShock.cs:1710 #, csharp-format msgid "{0} has joined. IP: {1}" msgstr "" @@ -615,7 +615,7 @@ msgstr "" msgid "{0} has launched himself into space." msgstr "" -#: ../../TShockAPI/TShock.cs:1397 +#: ../../TShockAPI/TShock.cs:1433 #, csharp-format msgid "{0} has left." msgstr "" @@ -630,22 +630,22 @@ msgstr "" msgid "{0} has respawned you." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3555 +#: ../../TShockAPI/GetDataHandlers.cs:3596 #, csharp-format msgid "{0} has sent a request to the bunny delivery service!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3561 +#: ../../TShockAPI/GetDataHandlers.cs:3602 #, csharp-format msgid "{0} has sent a request to the cat delivery service!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3558 +#: ../../TShockAPI/GetDataHandlers.cs:3599 #, csharp-format msgid "{0} has sent a request to the dog delivery service!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3552 +#: ../../TShockAPI/GetDataHandlers.cs:3593 #, csharp-format msgid "{0} has sent a request to the slime delivery service!" msgstr "" @@ -662,7 +662,7 @@ msgstr[1] "" msgid "{0} has spawned a Wall of Flesh." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2620 +#: ../../TShockAPI/GetDataHandlers.cs:2655 #, csharp-format msgid "" "{0} has SSC data in the database, but has the tshock.ignore.ssc permission. " @@ -749,7 +749,7 @@ msgstr "" msgid "{0} is banned! Remove it!" msgstr "" -#: ../../TShockAPI/Commands.cs:6751 +#: ../../TShockAPI/Commands.cs:6747 #, csharp-format msgid "{0} is no longer in god mode." msgstr "" @@ -771,7 +771,7 @@ msgstr "" msgid "{0} is not dead!" msgstr "" -#: ../../TShockAPI/Commands.cs:6750 +#: ../../TShockAPI/Commands.cs:6746 #, csharp-format msgid "{0} is now in god mode." msgstr "" @@ -787,7 +787,7 @@ msgstr "" msgid "{0} just killed you!" msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1926 +#: ../../TShockAPI/TSPlayer.cs:2013 #, csharp-format msgid "{0} kicked {1} for '{2}'" msgstr "" @@ -887,8 +887,8 @@ msgstr "" msgid "{0} started the frost moon at wave {1}!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4150 -#: ../../TShockAPI/GetDataHandlers.cs:4152 +#: ../../TShockAPI/GetDataHandlers.cs:4194 +#: ../../TShockAPI/GetDataHandlers.cs:4196 #, csharp-format msgid "{0} started the Old One's Army event!" msgstr "" @@ -923,63 +923,63 @@ msgstr "" msgid "{0} successfully deleted account: {1}." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3567 +#: ../../TShockAPI/GetDataHandlers.cs:3608 #, csharp-format msgid "{0} summoned a Blood Moon!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3579 +#: ../../TShockAPI/GetDataHandlers.cs:3620 #, csharp-format msgid "{0} summoned a frost moon!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3591 +#: ../../TShockAPI/GetDataHandlers.cs:3632 #, csharp-format msgid "{0} summoned a Goblin Invasion!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3573 +#: ../../TShockAPI/GetDataHandlers.cs:3614 #, csharp-format msgid "{0} summoned a Martian invasion!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3549 +#: ../../TShockAPI/GetDataHandlers.cs:3590 #, csharp-format msgid "{0} summoned a Mechdusa!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3570 +#: ../../TShockAPI/GetDataHandlers.cs:3611 #, csharp-format msgid "{0} summoned a Moon Lord!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3582 +#: ../../TShockAPI/GetDataHandlers.cs:3623 #, csharp-format msgid "{0} summoned a pumpkin moon!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3576 +#: ../../TShockAPI/GetDataHandlers.cs:3617 #, csharp-format msgid "{0} summoned an eclipse!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3598 +#: ../../TShockAPI/GetDataHandlers.cs:3639 #, csharp-format msgid "{0} summoned the {1}!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2977 -#: ../../TShockAPI/GetDataHandlers.cs:2980 +#: ../../TShockAPI/GetDataHandlers.cs:3016 +#: ../../TShockAPI/GetDataHandlers.cs:3019 #, csharp-format msgid "{0} summoned the Empress of Light!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3585 +#: ../../TShockAPI/GetDataHandlers.cs:3626 #, csharp-format msgid "{0} summoned the Pirates!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3588 +#: ../../TShockAPI/GetDataHandlers.cs:3629 #, csharp-format msgid "{0} summoned the Snow Legion!" msgstr "" @@ -1021,12 +1021,12 @@ msgstr "" msgid "{0} warped you to {1}." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1953 +#: ../../TShockAPI/TSPlayer.cs:2040 #, csharp-format msgid "{0} was banned for '{1}'." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1924 +#: ../../TShockAPI/TSPlayer.cs:2011 #, csharp-format msgid "{0} was kicked for '{1}'" msgstr "" @@ -1216,7 +1216,7 @@ msgid "" "re-create the allowed field." msgstr "" -#: ../../TShockAPI/TShock.cs:986 +#: ../../TShockAPI/TShock.cs:1022 msgid "" "A malicious server can easily steal a user's UUID. You may consider turning " "this option off if you run a public server." @@ -1234,7 +1234,7 @@ msgstr "" msgid "a Nebula Pillar" msgstr "" -#: ../../TShockAPI/TShock.cs:974 +#: ../../TShockAPI/TShock.cs:1010 msgid "A password for this server was set in config.json and is being used." msgstr "" @@ -1242,7 +1242,7 @@ msgstr "" msgid "A player name must be provided to kick a player. Please provide one." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2451 +#: ../../TShockAPI/GetDataHandlers.cs:2486 msgid "A plugin on this server stopped your login." msgstr "" @@ -1398,7 +1398,7 @@ msgstr "" msgid "Amethyst Gemtree" msgstr "" -#: ../../TShockAPI/TShock.cs:996 +#: ../../TShockAPI/TShock.cs:1032 msgid "" "An account has been detected in the user database, but setup-code.txt is " "still present." @@ -1414,7 +1414,7 @@ msgstr "" msgid "An exception has occurred during database transaction: {0}" msgstr "" -#: ../../TShockAPI/TShock.cs:1489 +#: ../../TShockAPI/TShock.cs:1525 msgid "An exception occurred executing a command." msgstr "" @@ -1477,19 +1477,19 @@ msgid "" msgstr "" #: ../../TShockAPI/Commands.cs:903 -#: ../../TShockAPI/GetDataHandlers.cs:2636 -#: ../../TShockAPI/GetDataHandlers.cs:3211 +#: ../../TShockAPI/GetDataHandlers.cs:2671 +#: ../../TShockAPI/GetDataHandlers.cs:3251 #, csharp-format msgid "Authenticated as {0} successfully." msgstr "" -#: ../../TShockAPI/TShock.cs:440 -#: ../../TShockAPI/TShock.cs:1590 +#: ../../TShockAPI/TShock.cs:471 +#: ../../TShockAPI/TShock.cs:1626 msgid "AutoSave Disabled" msgstr "" -#: ../../TShockAPI/TShock.cs:438 -#: ../../TShockAPI/TShock.cs:1588 +#: ../../TShockAPI/TShock.cs:469 +#: ../../TShockAPI/TShock.cs:1624 msgid "AutoSave Enabled" msgstr "" @@ -1538,11 +1538,11 @@ msgstr "" msgid "Backup Thread" msgstr "" -#: ../../TShockAPI/TShock.cs:444 +#: ../../TShockAPI/TShock.cs:475 msgid "Backups Disabled" msgstr "" -#: ../../TShockAPI/TShock.cs:442 +#: ../../TShockAPI/TShock.cs:473 msgid "Backups Enabled" msgstr "" @@ -1625,7 +1625,7 @@ msgstr "" msgid "Banned tile {0}." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1950 +#: ../../TShockAPI/TSPlayer.cs:2037 #, csharp-format msgid "Banned: {0}" msgstr "" @@ -1660,12 +1660,12 @@ msgstr "" msgid "Boreal Tree" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3264 +#: ../../TShockAPI/GetDataHandlers.cs:3304 #, csharp-format msgid "Bouncer / HandleNpcTalk rejected from bouncer out of bounds from {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3257 +#: ../../TShockAPI/GetDataHandlers.cs:3297 #, csharp-format msgid "Bouncer / HandleNpcTalk rejected from bouncer throttle from {0}" msgstr "" @@ -1705,57 +1705,57 @@ msgstr "" msgid "Bouncer / OnChestOpen rejected from region check from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2735 +#: ../../TShockAPI/Bouncer.cs:2738 #, csharp-format msgid "" "Bouncer / OnFishOutNPC rejected for not finding active bobber projectile! - " "From {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2729 +#: ../../TShockAPI/Bouncer.cs:2732 #, csharp-format msgid "Bouncer / OnFishOutNPC rejected for not using a fishing rod! - From {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2741 +#: ../../TShockAPI/Bouncer.cs:2744 #, csharp-format msgid "" "Bouncer / OnFishOutNPC rejected for the NPC not being on the fishable NPCs " "list! - From {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2753 +#: ../../TShockAPI/Bouncer.cs:2756 #, csharp-format msgid "Bouncer / OnFishOutNPC rejected range checks from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2747 +#: ../../TShockAPI/Bouncer.cs:2750 #, csharp-format msgid "Bouncer / OnFishOutNPC rejected summon boss permissions from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2781 +#: ../../TShockAPI/Bouncer.cs:2784 #, csharp-format msgid "Bouncer / OnFoodPlatterTryPlacing rejected disabled from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2774 +#: ../../TShockAPI/Bouncer.cs:2777 #, csharp-format msgid "" "Bouncer / OnFoodPlatterTryPlacing rejected item not placed by hand from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2792 +#: ../../TShockAPI/Bouncer.cs:2795 #, csharp-format msgid "Bouncer / OnFoodPlatterTryPlacing rejected permissions from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2803 +#: ../../TShockAPI/Bouncer.cs:2806 #, csharp-format msgid "Bouncer / OnFoodPlatterTryPlacing rejected range checks from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2767 +#: ../../TShockAPI/Bouncer.cs:2770 #, csharp-format msgid "" "Bouncer / OnFoodPlatterTryPlacing rejected tile placement valid from {0}" @@ -1852,22 +1852,22 @@ msgstr "" msgid "Bouncer / OnItemDrop rejected from sneaky from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2703 +#: ../../TShockAPI/Bouncer.cs:2706 #, csharp-format msgid "Bouncer / OnKillMe rejected bad length death text from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2710 +#: ../../TShockAPI/Bouncer.cs:2713 #, csharp-format msgid "Bouncer / OnKillMe rejected custom death message from {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2684 +#: ../../TShockAPI/Bouncer.cs:2687 #, csharp-format msgid "Bouncer / OnKillMe rejected high damage from {0} {1}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2693 +#: ../../TShockAPI/Bouncer.cs:2696 #, csharp-format msgid "Bouncer / OnKillMe rejected index check from {0}" msgstr "" @@ -2280,7 +2280,7 @@ msgid "" "ticks: target is null" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2667 +#: ../../TShockAPI/Bouncer.cs:2670 #, csharp-format msgid "Bouncer / OnPlayerDamage rejected custom death message from {0}" msgstr "" @@ -2596,59 +2596,51 @@ msgstr "" msgid "Bouncer / OnUpdateNPCHome rejected range checks from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:558 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:413 #, csharp-format msgid "Bouncer / SendTileRect accepted clientside world edit from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:414 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:468 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:488 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:500 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:511 #, csharp-format -msgid "" -"Bouncer / SendTileRect processing a tile conversion update - [{0}] -> [{1}]" +msgid "Bouncer / SendTileRect reimplemented from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:429 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:446 #, csharp-format -msgid "" -"Bouncer / SendTileRect processing a wall conversion update - [{0}] -> [{1}]" +msgid "Bouncer / SendTileRect rejected from being disabled from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:129 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:518 #, csharp-format -msgid "Bouncer / SendTileRect reimplemented from carbonara from {0}" -msgstr "" - -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:299 -msgid "Bouncer / SendTileRect rejected for banned tile" +msgid "Bouncer / SendTileRect rejected from matches from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:578 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:459 #, csharp-format -msgid "Bouncer / SendTileRect rejected from being disabled from {0}" +msgid "" +"Bouncer / SendTileRect rejected from out of bounds / build permission from " +"{0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:293 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:478 #, csharp-format -msgid "" -"Bouncer / SendTileRect rejected from no permission for tile object from {0}" +msgid "Bouncer / SendTileRect rejected from out of range from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:565 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:427 #, csharp-format -msgid "Bouncer / SendTileRect rejected from non-vanilla tilemod from {0}" +msgid "Bouncer / SendTileRect rejected from size from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:571 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:436 #, csharp-format msgid "Bouncer / SendTileRect rejected from throttle from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:610 -msgid "" -"Bouncer / SendTileRectHandler - rejected tile object because object " -"dimensions fall outside the tile rect (excessive size)" -msgstr "" - #: ../../TShockAPI/Utils.cs:136 #, csharp-format msgid "Broadcast: {0}" @@ -2671,7 +2663,7 @@ msgstr "" msgid "Butcher Syntax and Example" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2619 +#: ../../TShockAPI/GetDataHandlers.cs:2654 msgid "" "Bypass SSC is enabled for your account. SSC data will not be loaded or saved." msgstr "" @@ -2780,7 +2772,7 @@ msgstr "" msgid "Commands: add, del, hide, list, send, [warpname]." msgstr "" -#: ../../TShockAPI/TShock.cs:765 +#: ../../TShockAPI/TShock.cs:801 #, csharp-format msgid "Config path has been set to {0}" msgstr "" @@ -2795,7 +2787,7 @@ msgstr "" msgid "Connect to the internet to figure out what to download?" msgstr "" -#: ../../TShockAPI/TShock.cs:1329 +#: ../../TShockAPI/TShock.cs:1365 msgid "Connecting via a proxy is not allowed." msgstr "" @@ -2808,7 +2800,7 @@ msgstr "" msgid "Corruption Palm" msgstr "" -#: ../../TShockAPI/TShock.cs:296 +#: ../../TShockAPI/TShock.cs:300 #, csharp-format msgid "" "Could not apply the given log path / log format, defaults will be used. " @@ -2886,7 +2878,7 @@ msgstr "" msgid "Could not rename {0}!" msgstr "" -#: ../../TShockAPI/TShock.cs:1448 +#: ../../TShockAPI/TShock.cs:1484 msgid "Crash attempt via long chat packet." msgstr "" @@ -2928,12 +2920,12 @@ msgstr "" msgid "Current spawn rate: {0}." msgstr "" -#: ../../TShockAPI/Bouncer.cs:2686 +#: ../../TShockAPI/Bouncer.cs:2689 #, csharp-format msgid "Death Exploit Attempt: Damage {0}" msgstr "" -#: ../../TShockAPI/Bouncer.cs:2704 +#: ../../TShockAPI/Bouncer.cs:2707 msgid "Death reason outside of normal bounds." msgstr "" @@ -3134,7 +3126,7 @@ msgstr "" msgid "Executes a command as the super admin." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4371 +#: ../../TShockAPI/GetDataHandlers.cs:4416 msgid "Exploit attempt detected!" msgstr "" @@ -3177,7 +3169,7 @@ msgstr "" msgid "Failed to rename the region." msgstr "" -#: ../../TShockAPI/Bouncer.cs:2685 +#: ../../TShockAPI/Bouncer.cs:2688 msgid "Failed to shade polygon normals." msgstr "" @@ -3239,402 +3231,402 @@ msgid_plural "Gave {0} {1}s." msgstr[0] "" msgstr[1] "" -#: ../../TShockAPI/GetDataHandlers.cs:3796 +#: ../../TShockAPI/GetDataHandlers.cs:3839 #, csharp-format msgid "GetDataHandlers / HandleCatchNpc catch zero {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3804 +#: ../../TShockAPI/GetDataHandlers.cs:3847 #, csharp-format msgid "GetDataHandlers / HandleCatchNpc rejected catch npc {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3119 +#: ../../TShockAPI/GetDataHandlers.cs:3158 #, csharp-format msgid "" "GetDataHandlers / HandleChestActive rejected build permission and region " "check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3094 +#: ../../TShockAPI/GetDataHandlers.cs:3133 #, csharp-format msgid "GetDataHandlers / HandleChestItem rejected max stacks {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2844 +#: ../../TShockAPI/GetDataHandlers.cs:2881 #, csharp-format msgid "GetDataHandlers / HandleDoorUse rejected door gap check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2829 +#: ../../TShockAPI/GetDataHandlers.cs:2866 #, csharp-format msgid "GetDataHandlers / HandleDoorUse rejected out of range door {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2835 +#: ../../TShockAPI/GetDataHandlers.cs:2872 #, csharp-format msgid "GetDataHandlers / HandleDoorUse rejected type 0 5 check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2669 +#: ../../TShockAPI/GetDataHandlers.cs:2704 msgid "GetDataHandlers / HandleGetSection rejected reserve slot" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4042 +#: ../../TShockAPI/GetDataHandlers.cs:4086 #, csharp-format msgid "GetDataHandlers / HandleKillPortal rejected owner mismatch check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2989 +#: ../../TShockAPI/GetDataHandlers.cs:3028 #, csharp-format msgid "GetDataHandlers / HandleNpcStrike rejected Cultist summon from {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2972 +#: ../../TShockAPI/GetDataHandlers.cs:3011 #, csharp-format msgid "GetDataHandlers / HandleNpcStrike rejected EoL summon from {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2962 +#: ../../TShockAPI/GetDataHandlers.cs:3001 #, csharp-format msgid "GetDataHandlers / HandleNpcStrike rejected npc strike {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3250 +#: ../../TShockAPI/GetDataHandlers.cs:3290 #, csharp-format msgid "GetDataHandlers / HandleNpcTalk rejected npc talk {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4086 +#: ../../TShockAPI/GetDataHandlers.cs:4130 #, csharp-format msgid "" "GetDataHandlers / HandleNpcTeleportPortal rejected not thinking with portals " "{0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4079 +#: ../../TShockAPI/GetDataHandlers.cs:4123 #, csharp-format msgid "GetDataHandlers / HandleNpcTeleportPortal rejected null check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3920 +#: ../../TShockAPI/GetDataHandlers.cs:3963 #, csharp-format msgid "" "GetDataHandlers / HandleNumberOfAnglerQuestsCompleted surprise packet! " "Someone tell the TShock team! {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4144 +#: ../../TShockAPI/GetDataHandlers.cs:4188 #, csharp-format msgid "GetDataHandlers / HandleOldOnesArmy rejected permissions {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4138 +#: ../../TShockAPI/GetDataHandlers.cs:4182 #, csharp-format msgid "GetDataHandlers / HandleOldOnesArmy rejected throttled {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3622 +#: ../../TShockAPI/GetDataHandlers.cs:3664 #, csharp-format msgid "GetDataHandlers / HandlePaintTile rejected range check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3640 +#: ../../TShockAPI/GetDataHandlers.cs:3682 #, csharp-format msgid "GetDataHandlers / HandlePaintTile rejected select consistency {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3649 +#: ../../TShockAPI/GetDataHandlers.cs:3691 #, csharp-format msgid "" "GetDataHandlers / HandlePaintTile rejected throttle/permission/range check " "{0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3669 +#: ../../TShockAPI/GetDataHandlers.cs:3712 #, csharp-format msgid "GetDataHandlers / HandlePaintWall rejected range check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3687 +#: ../../TShockAPI/GetDataHandlers.cs:3730 #, csharp-format msgid "GetDataHandlers / HandlePaintWall rejected selector consistency {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3696 +#: ../../TShockAPI/GetDataHandlers.cs:3739 #, csharp-format msgid "" "GetDataHandlers / HandlePaintWall rejected throttle/permission/range {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3411 +#: ../../TShockAPI/GetDataHandlers.cs:3452 #, csharp-format msgid "GetDataHandlers / HandlePlayerBuffList handled event and sent data {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3396 +#: ../../TShockAPI/GetDataHandlers.cs:3437 #, csharp-format msgid "" "GetDataHandlers / HandlePlayerBuffList zeroed player buff due to below state " "2 {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2777 +#: ../../TShockAPI/GetDataHandlers.cs:2814 #, csharp-format msgid "GetDataHandlers / HandlePlayerHp rejected over max hp {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2522 +#: ../../TShockAPI/GetDataHandlers.cs:2557 msgid "GetDataHandlers / HandlePlayerInfo rejected hardcore required" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2516 +#: ../../TShockAPI/GetDataHandlers.cs:2551 msgid "GetDataHandlers / HandlePlayerInfo rejected mediumcore required" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2457 +#: ../../TShockAPI/GetDataHandlers.cs:2492 msgid "GetDataHandlers / HandlePlayerInfo rejected name length 0" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2450 +#: ../../TShockAPI/GetDataHandlers.cs:2485 #, csharp-format msgid "GetDataHandlers / HandlePlayerInfo rejected plugin phase {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2510 +#: ../../TShockAPI/GetDataHandlers.cs:2545 msgid "GetDataHandlers / HandlePlayerInfo rejected softcore required" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4210 -#: ../../TShockAPI/GetDataHandlers.cs:4216 +#: ../../TShockAPI/GetDataHandlers.cs:4255 +#: ../../TShockAPI/GetDataHandlers.cs:4261 #, csharp-format msgid "GetDataHandlers / HandlePlayerKillMeV2 kicked with difficulty {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4225 +#: ../../TShockAPI/GetDataHandlers.cs:4270 #, csharp-format msgid "GetDataHandlers / HandlePlayerKillMeV2 ssc delete {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3289 +#: ../../TShockAPI/GetDataHandlers.cs:3329 #, csharp-format msgid "GetDataHandlers / HandlePlayerMana rejected max mana {0} {1}/{2}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2554 +#: ../../TShockAPI/GetDataHandlers.cs:2589 msgid "GetDataHandlers / HandlePlayerSlot rejected ignore ssc packets" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3316 +#: ../../TShockAPI/GetDataHandlers.cs:3357 #, csharp-format msgid "GetDataHandlers / HandlePlayerTeam rejected team fastswitch {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2757 +#: ../../TShockAPI/GetDataHandlers.cs:2794 #, csharp-format msgid "GetDataHandlers / HandlePlayerUpdate home position delta {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3144 +#: ../../TShockAPI/GetDataHandlers.cs:3183 msgid "GetDataHandlers / HandlePlayerZone rejected null check" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3025 +#: ../../TShockAPI/GetDataHandlers.cs:3064 #, csharp-format msgid "" "GetDataHandlers / HandleProjectileKill permitted skeletron prime exemption " "{0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3029 +#: ../../TShockAPI/GetDataHandlers.cs:3068 #, csharp-format msgid "GetDataHandlers / HandleProjectileKill rejected banned projectile {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3014 +#: ../../TShockAPI/GetDataHandlers.cs:3053 #, csharp-format msgid "GetDataHandlers / HandleProjectileKill rejected tombstone {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3354 +#: ../../TShockAPI/GetDataHandlers.cs:3395 #, csharp-format msgid "GetDataHandlers / HandleSign rejected sign on build permission {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3361 +#: ../../TShockAPI/GetDataHandlers.cs:3402 #, csharp-format msgid "GetDataHandlers / HandleSign rejected sign range check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3334 +#: ../../TShockAPI/GetDataHandlers.cs:3375 #, csharp-format msgid "GetDataHandlers / HandleSignRead rejected out of bounds {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2700 +#: ../../TShockAPI/GetDataHandlers.cs:2737 #, csharp-format msgid "GetDataHandlers / HandleSpawn force teleport 'vanilla spawn' {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2711 +#: ../../TShockAPI/GetDataHandlers.cs:2748 #, csharp-format msgid "GetDataHandlers / HandleSpawn force teleport phase 1 {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2720 +#: ../../TShockAPI/GetDataHandlers.cs:2757 #, csharp-format msgid "GetDataHandlers / HandleSpawn force teleport phase 2 {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2682 +#: ../../TShockAPI/GetDataHandlers.cs:2717 #, csharp-format msgid "GetDataHandlers / HandleSpawn rejected dead player spawn request {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3517 +#: ../../TShockAPI/GetDataHandlers.cs:3558 #, csharp-format msgid "GetDataHandlers / HandleSpawnBoss rejected boss {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3507 +#: ../../TShockAPI/GetDataHandlers.cs:3548 #, csharp-format msgid "GetDataHandlers / HandleSpawnBoss rejected bouner throttled {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3524 +#: ../../TShockAPI/GetDataHandlers.cs:3565 #, csharp-format msgid "GetDataHandlers / HandleSpawnBoss rejected invasion {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3531 +#: ../../TShockAPI/GetDataHandlers.cs:3572 #, csharp-format msgid "GetDataHandlers / HandleSpawnBoss rejected pet {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3442 +#: ../../TShockAPI/GetDataHandlers.cs:3483 #, csharp-format msgid "" "GetDataHandlers / HandleSpecial rejected enchanted sundial permission " "(ForceTime) {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3436 +#: ../../TShockAPI/GetDataHandlers.cs:3477 #, csharp-format msgid "" "GetDataHandlers / HandleSpecial rejected enchanted sundial permission {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3426 +#: ../../TShockAPI/GetDataHandlers.cs:3467 #, csharp-format msgid "GetDataHandlers / HandleSpecial rejected type 1 for {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4005 +#: ../../TShockAPI/GetDataHandlers.cs:4049 #, csharp-format msgid "" "GetDataHandlers / HandleSyncExtraValue rejected expert/master mode check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3999 +#: ../../TShockAPI/GetDataHandlers.cs:4043 #, csharp-format msgid "GetDataHandlers / HandleSyncExtraValue rejected extents check {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4011 +#: ../../TShockAPI/GetDataHandlers.cs:4055 #, csharp-format msgid "" "GetDataHandlers / HandleSyncExtraValue rejected npc id out of bounds check - " "NPC ID: {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4018 +#: ../../TShockAPI/GetDataHandlers.cs:4062 #, csharp-format msgid "" "GetDataHandlers / HandleSyncExtraValue rejected npc is null - NPC ID: {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4025 +#: ../../TShockAPI/GetDataHandlers.cs:4069 #, csharp-format msgid "" "GetDataHandlers / HandleSyncExtraValue rejected range check {0},{1} vs " "{2},{3} which is {4}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4397 +#: ../../TShockAPI/GetDataHandlers.cs:4442 #, csharp-format msgid "GetDataHandlers / HandleSyncLoadout rejected loadout index sync {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4388 +#: ../../TShockAPI/GetDataHandlers.cs:4433 #, csharp-format msgid "" "GetDataHandlers / HandleSyncLoadout rejected loadout index sync out of bounds " "{0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3753 +#: ../../TShockAPI/GetDataHandlers.cs:3796 #, csharp-format msgid "GetDataHandlers / HandleTeleport rejected npc teleport {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3762 +#: ../../TShockAPI/GetDataHandlers.cs:3805 #, csharp-format msgid "GetDataHandlers / HandleTeleport rejected p2p extents {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3768 +#: ../../TShockAPI/GetDataHandlers.cs:3811 #, csharp-format msgid "" "GetDataHandlers / HandleTeleport rejected p2p wormhole permission {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3744 +#: ../../TShockAPI/GetDataHandlers.cs:3787 #, csharp-format msgid "GetDataHandlers / HandleTeleport rejected rod type {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3842 -#: ../../TShockAPI/GetDataHandlers.cs:3858 -#: ../../TShockAPI/GetDataHandlers.cs:3881 +#: ../../TShockAPI/GetDataHandlers.cs:3885 #: ../../TShockAPI/GetDataHandlers.cs:3901 +#: ../../TShockAPI/GetDataHandlers.cs:3924 +#: ../../TShockAPI/GetDataHandlers.cs:3944 #, csharp-format msgid "" "GetDataHandlers / HandleTeleportationPotion rejected not holding the correct " "item {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3832 +#: ../../TShockAPI/GetDataHandlers.cs:3875 #, csharp-format msgid "" "GetDataHandlers / HandleTeleportationPotion rejected permissions {0} {1}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4126 +#: ../../TShockAPI/GetDataHandlers.cs:4170 #, csharp-format msgid "GetDataHandlers / HandleToggleParty rejected no party {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3059 +#: ../../TShockAPI/GetDataHandlers.cs:3098 #, csharp-format msgid "GetDataHandlers / HandleTogglePvp rejected fastswitch {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3052 +#: ../../TShockAPI/GetDataHandlers.cs:3091 #, csharp-format msgid "GetDataHandlers / HandleTogglePvp rejected index mismatch {0}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2735 +#: ../../TShockAPI/GetDataHandlers.cs:2772 msgid "GetDataHandlers / OnPlayerUpdate rejected from null player." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2464 +#: ../../TShockAPI/GetDataHandlers.cs:2499 msgid "" "GetDataHandlers / rejecting player for name prefix starting with tsi: or " "tsn:." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3492 +#: ../../TShockAPI/GetDataHandlers.cs:3533 #, csharp-format msgid "GetDataHandlers / UpdateNPCHome rejected no permission {0}" msgstr "" @@ -3801,7 +3793,7 @@ msgstr "" msgid "Hallow Palm" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4372 +#: ../../TShockAPI/GetDataHandlers.cs:4417 #, csharp-format msgid "" "HandleSyncCavernMonsterType: Player is trying to modify NPC " @@ -3903,7 +3895,7 @@ msgid "" "were disabled for to TShock so we can improve this!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3026 +#: ../../TShockAPI/GetDataHandlers.cs:3065 msgid "" "If this was not skeletron prime related, please report to TShock what " "happened." @@ -3964,7 +3956,7 @@ msgstr "" msgid "Ignoring shrapnel per config.." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2465 +#: ../../TShockAPI/GetDataHandlers.cs:2500 msgid "Illegal name: prefixes tsi: and tsn: are forbidden." msgstr "" @@ -4197,7 +4189,7 @@ msgid "" "Press any key to exit." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3231 +#: ../../TShockAPI/GetDataHandlers.cs:3271 msgid "Invalid server password." msgstr "" @@ -4660,7 +4652,7 @@ msgstr "" msgid "Item bans ({{0}}/{{1}}):" msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1920 +#: ../../TShockAPI/TSPlayer.cs:2007 #, csharp-format msgid "Kicked {0} for : '{1}'" msgstr "" @@ -4669,7 +4661,7 @@ msgstr "" msgid "Kicked via web" msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1919 +#: ../../TShockAPI/TSPlayer.cs:2006 #, csharp-format msgid "Kicked: {0}" msgstr "" @@ -4753,12 +4745,12 @@ msgstr "" msgid "List Online Players Syntax" msgstr "" -#: ../../TShockAPI/TShock.cs:828 +#: ../../TShockAPI/TShock.cs:864 #, csharp-format msgid "Listening on IP {0}." msgstr "" -#: ../../TShockAPI/TShock.cs:809 +#: ../../TShockAPI/TShock.cs:845 #, csharp-format msgid "Listening on port {0}." msgstr "" @@ -4775,7 +4767,7 @@ msgstr "" msgid "listusers - Lists all REST users and their current active tokens." msgstr "" -#: ../../TShockAPI/TShock.cs:798 +#: ../../TShockAPI/TShock.cs:834 #, csharp-format msgid "Loading dedicated config file: {0}" msgstr "" @@ -4793,7 +4785,7 @@ msgstr "" msgid "Log display enabled." msgstr "" -#: ../../TShockAPI/TShock.cs:785 +#: ../../TShockAPI/TShock.cs:821 #, csharp-format msgid "Log path has been set to {0}" msgstr "" @@ -4802,13 +4794,13 @@ msgstr "" msgid "Login attempt failed - see the message above." msgstr "" -#: ../../TShockAPI/TShock.cs:980 +#: ../../TShockAPI/TShock.cs:1016 msgid "" "Login before join enabled. Users may be prompted for an account specific " "password instead of a server password on connect." msgstr "" -#: ../../TShockAPI/TShock.cs:985 +#: ../../TShockAPI/TShock.cs:1021 msgid "Login using UUID enabled. Users automatically login via UUID." msgstr "" @@ -4928,7 +4920,7 @@ msgstr "" msgid "Mode: {0}" msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1966 +#: ../../TShockAPI/TSPlayer.cs:2053 msgid "More than one match found -- unable to decide which is correct: " msgstr "" @@ -4977,7 +4969,7 @@ msgstr "" msgid "New name is too large!" msgstr "" -#: ../../TShockAPI/TShock.cs:864 +#: ../../TShockAPI/TShock.cs:900 #, csharp-format msgid "New worlds will be generated with the {0} world evil type!" msgstr "" @@ -5055,9 +5047,9 @@ msgid "" "token was not valid." msgstr "" +#: ../../TShockAPI/Rest/SecureRest.cs:180 #: ../../TShockAPI/Rest/RestCommand.cs:95 #: ../../TShockAPI/Rest/RestCommand.cs:101 -#: ../../TShockAPI/Rest/SecureRest.cs:180 msgid "Not authorized. The specified API endpoint requires a token." msgstr "" @@ -5197,8 +5189,8 @@ msgstr "" msgid "Player \"{0}\" is already logged in." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1883 -#: ../../TShockAPI/TSPlayer.cs:1887 +#: ../../TShockAPI/TSPlayer.cs:1970 +#: ../../TShockAPI/TSPlayer.cs:1974 #, csharp-format msgid "Player {0} has been disabled for {1}." msgstr "" @@ -5260,7 +5252,7 @@ msgstr "" msgid "Player not found. Unable to kick the player." msgstr "" -#: ../../TShockAPI/TShock.cs:1696 +#: ../../TShockAPI/TShock.cs:1732 #, csharp-format msgid "Please {0}register or {0}login to play!" msgstr "" @@ -5286,7 +5278,7 @@ msgstr "" msgid "Please use the following to create a permanent account for you." msgstr "" -#: ../../TShockAPI/TShock.cs:895 +#: ../../TShockAPI/TShock.cs:931 #, csharp-format msgid "Port overridden by startup argument. Set to {0}" msgstr "" @@ -5363,12 +5355,12 @@ msgstr "" msgid "Quick usage: {0} {1} \"Griefing\"" msgstr "" -#: ../../TShockAPI/TSPlayer.cs:758 +#: ../../TShockAPI/TSPlayer.cs:759 #, csharp-format msgid "Rangecheck failed for {0} ({1}, {2}) (rg: {3}/{5}, {4}/{5})" msgstr "" -#: ../../TShockAPI/TShock.cs:1196 +#: ../../TShockAPI/TShock.cs:1232 msgid "Reached HealOtherPlayer threshold" msgstr "" @@ -5376,7 +5368,7 @@ msgstr "" msgid "Reached HealOtherPlayer threshold." msgstr "" -#: ../../TShockAPI/TShock.cs:1187 +#: ../../TShockAPI/TShock.cs:1223 msgid "Reached paint threshold" msgstr "" @@ -5384,16 +5376,16 @@ msgstr "" msgid "Reached projectile create threshold." msgstr "" -#: ../../TShockAPI/TShock.cs:1178 +#: ../../TShockAPI/TShock.cs:1214 msgid "Reached projectile threshold" msgstr "" +#: ../../TShockAPI/TShock.cs:1144 #: ../../TShockAPI/Bouncer.cs:922 -#: ../../TShockAPI/TShock.cs:1108 msgid "Reached TileKill threshold." msgstr "" -#: ../../TShockAPI/TShock.cs:1169 +#: ../../TShockAPI/TShock.cs:1205 msgid "Reached TileLiquid threshold" msgstr "" @@ -5406,7 +5398,7 @@ msgstr "" msgid "Reached TileLiquid threshold." msgstr "" -#: ../../TShockAPI/TShock.cs:1125 +#: ../../TShockAPI/TShock.cs:1161 msgid "Reached TilePlace threshold" msgstr "" @@ -5670,7 +5662,7 @@ msgstr "" msgid "Sends you to your spawn point." msgstr "" -#: ../../TShockAPI/TShock.cs:738 +#: ../../TShockAPI/TShock.cs:774 msgid "Server console interrupted!" msgstr "" @@ -5682,7 +5674,7 @@ msgstr "" msgid "Server is full. No reserved slots open." msgstr "" -#: ../../TShockAPI/TShock.cs:1300 +#: ../../TShockAPI/TShock.cs:1336 msgid "Server is shutting down..." msgstr "" @@ -5712,7 +5704,7 @@ msgstr "" msgid "Server side characters are enabled. You need to be logged-in to play." msgstr "" -#: ../../TShockAPI/TShock.cs:1691 +#: ../../TShockAPI/TShock.cs:1727 #, csharp-format msgid "" "Server side characters is enabled! Please {0}register or {0}login to play!" @@ -5841,7 +5833,7 @@ msgstr "" msgid "Shuts down the server without saving." msgstr "" -#: ../../TShockAPI/TShock.cs:735 +#: ../../TShockAPI/TShock.cs:771 msgid "Shutting down safely. To force shutdown, send SIGINT (CTRL + C) again." msgstr "" @@ -5858,7 +5850,7 @@ msgstr "" msgid "Skeletron Prime" msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1069 +#: ../../TShockAPI/TSPlayer.cs:1076 #, csharp-format msgid "Skipping SSC save (due to tshock.ignore.ssc) for {0}" msgstr "" @@ -5939,67 +5931,67 @@ msgstr "" msgid "SSC must be enabled to use this command." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:434 +#: ../../TShockAPI/TSPlayer.cs:435 #, csharp-format msgid "Stack cheat detected. Remove armor {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:577 +#: ../../TShockAPI/TSPlayer.cs:578 #, csharp-format msgid "" "Stack cheat detected. Remove Defender's Forge item {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:454 +#: ../../TShockAPI/TSPlayer.cs:455 #, csharp-format msgid "Stack cheat detected. Remove dye {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:414 -#: ../../TShockAPI/TSPlayer.cs:474 +#: ../../TShockAPI/TSPlayer.cs:415 +#: ../../TShockAPI/TSPlayer.cs:475 #, csharp-format msgid "Stack cheat detected. Remove item {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:494 +#: ../../TShockAPI/TSPlayer.cs:495 #, csharp-format msgid "Stack cheat detected. Remove item dye {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:618 -#: ../../TShockAPI/TSPlayer.cs:638 +#: ../../TShockAPI/TSPlayer.cs:619 +#: ../../TShockAPI/TSPlayer.cs:639 #, csharp-format msgid "Stack cheat detected. Remove Loadout 1 item {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:658 -#: ../../TShockAPI/TSPlayer.cs:678 +#: ../../TShockAPI/TSPlayer.cs:659 +#: ../../TShockAPI/TSPlayer.cs:679 #, csharp-format msgid "Stack cheat detected. Remove Loadout 2 item {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:698 -#: ../../TShockAPI/TSPlayer.cs:718 +#: ../../TShockAPI/TSPlayer.cs:699 +#: ../../TShockAPI/TSPlayer.cs:719 #, csharp-format msgid "Stack cheat detected. Remove Loadout 3 item {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:515 +#: ../../TShockAPI/TSPlayer.cs:516 #, csharp-format msgid "Stack cheat detected. Remove piggy-bank item {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:536 +#: ../../TShockAPI/TSPlayer.cs:537 #, csharp-format msgid "Stack cheat detected. Remove safe item {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:556 +#: ../../TShockAPI/TSPlayer.cs:557 #, csharp-format msgid "Stack cheat detected. Remove trash item {0} ({1}) and then rejoin." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:598 +#: ../../TShockAPI/TSPlayer.cs:599 #, csharp-format msgid "Stack cheat detected. Remove Void Vault item {0} ({1}) and then rejoin." msgstr "" @@ -6016,19 +6008,19 @@ msgstr "" msgid "Started an eclipse." msgstr "" -#: ../../TShockAPI/TShock.cs:927 +#: ../../TShockAPI/TShock.cs:963 msgid "Startup parameter overrode maximum player slot configuration value." msgstr "" -#: ../../TShockAPI/TShock.cs:909 +#: ../../TShockAPI/TShock.cs:945 msgid "Startup parameter overrode REST enable." msgstr "" -#: ../../TShockAPI/TShock.cs:918 +#: ../../TShockAPI/TShock.cs:954 msgid "Startup parameter overrode REST port." msgstr "" -#: ../../TShockAPI/TShock.cs:901 +#: ../../TShockAPI/TShock.cs:937 msgid "Startup parameter overrode REST token." msgstr "" @@ -6197,7 +6189,7 @@ msgstr "" msgid "the Destroyer" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3427 +#: ../../TShockAPI/GetDataHandlers.cs:3468 msgid "The Dungeon Guardian returned you to your spawn point." msgstr "" @@ -6431,8 +6423,8 @@ msgid_plural "These are the plugins you requested to install" msgstr[0] "" msgstr[1] "" -#: ../../TShockAPI/TShock.cs:1013 -#: ../../TShockAPI/TShock.cs:1023 +#: ../../TShockAPI/TShock.cs:1049 +#: ../../TShockAPI/TShock.cs:1059 #, csharp-format msgid "This token will display until disabled by verification. ({0}setup)" msgstr "" @@ -6486,8 +6478,8 @@ msgid "" "instead of {1}" msgstr "" -#: ../../TShockAPI/TShock.cs:1012 -#: ../../TShockAPI/TShock.cs:1022 +#: ../../TShockAPI/TShock.cs:1048 +#: ../../TShockAPI/TShock.cs:1058 #, csharp-format msgid "To setup the server, join the game and type {0}setup {1}" msgstr "" @@ -6554,7 +6546,7 @@ msgstr "" msgid "Tried to grow a {0}." msgstr "" -#: ../../TShockAPI/TShock.cs:399 +#: ../../TShockAPI/TShock.cs:403 #, csharp-format msgid "TShock {0} ({1}) now running." msgstr "" @@ -6563,29 +6555,29 @@ msgstr "" msgid "TShock Ban Help" msgstr "" -#: ../../TShockAPI/TShock.cs:449 +#: ../../TShockAPI/TShock.cs:480 msgid "TShock comes with no warranty & is free software." msgstr "" -#: ../../TShockAPI/TShock.cs:461 +#: ../../TShockAPI/TShock.cs:492 msgid "" "TShock encountered a problem from which it cannot recover. The following " "message may help diagnose the problem." msgstr "" -#: ../../TShockAPI/TShock.cs:1021 +#: ../../TShockAPI/TShock.cs:1057 msgid "" "TShock Notice: setup-code.txt is still present, and the code located in that " "file will be used." msgstr "" -#: ../../TShockAPI/TShock.cs:353 +#: ../../TShockAPI/TShock.cs:357 msgid "" "TShock was improperly shut down. Please use the exit command in the future to " "prevent this." msgstr "" -#: ../../TShockAPI/TShock.cs:997 +#: ../../TShockAPI/TShock.cs:1033 msgid "" "TShock will now disable the initial setup system and remove setup-code.txt as " "it is no longer needed." @@ -6740,11 +6732,11 @@ msgstr "" msgid "Unable to launch {0} because she is not logged in." msgstr "" -#: ../../TShockAPI/TShock.cs:1484 +#: ../../TShockAPI/TShock.cs:1520 msgid "Unable to parse command '{0}' from player {1}." msgstr "" -#: ../../TShockAPI/TShock.cs:1483 +#: ../../TShockAPI/TShock.cs:1519 msgid "" "Unable to parse command. Please contact an administrator for assistance." msgstr "" @@ -6788,7 +6780,7 @@ msgstr "" msgid "Unrecognized player direction" msgstr "" -#: ../../TShockAPI/TShock.cs:462 +#: ../../TShockAPI/TShock.cs:493 msgid "" "Until the problem is resolved, TShock will not be able to start (and will " "crash on startup)." @@ -6835,11 +6827,11 @@ msgstr "" msgid "Use \"{0}worldevent rain slime\" to start slime rain!" msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1971 +#: ../../TShockAPI/TSPlayer.cs:2058 msgid "Use \"my query\" for items with spaces." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1972 +#: ../../TShockAPI/TSPlayer.cs:2059 msgid "" "Use tsi:[number] or tsn:[username] to distinguish between user IDs and " "usernames." @@ -6897,8 +6889,8 @@ msgid "" "privileges." msgstr "" -#: ../../TShockAPI/TShock.cs:390 #: ../../TShockAPI/TShock.cs:394 +#: ../../TShockAPI/TShock.cs:398 #, csharp-format msgid "Using {0} for tile implementation" msgstr "" @@ -7012,7 +7004,7 @@ msgstr "" msgid "Warps ({{0}}/{{1}}):" msgstr "" -#: ../../TShockAPI/TShock.cs:448 +#: ../../TShockAPI/TShock.cs:479 msgid "Welcome to TShock for Terraria!" msgstr "" @@ -7042,12 +7034,12 @@ msgstr "" msgid "World mode set to {0}." msgstr "" -#: ../../TShockAPI/TShock.cs:818 +#: ../../TShockAPI/TShock.cs:854 #, csharp-format msgid "World name will be overridden by: {0}" msgstr "" -#: ../../TShockAPI/TShock.cs:775 +#: ../../TShockAPI/TShock.cs:811 #, csharp-format msgid "World path has been set to {0}" msgstr "" @@ -7078,7 +7070,7 @@ msgstr "" msgid "You are dead. Dead players can't go home." msgstr "" -#: ../../TShockAPI/TShock.cs:1501 +#: ../../TShockAPI/TShock.cs:1537 msgid "You are muted!" msgstr "" @@ -7089,7 +7081,7 @@ msgstr "" msgid "You are muted." msgstr "" -#: ../../TShockAPI/Commands.cs:6758 +#: ../../TShockAPI/Commands.cs:6754 #, csharp-format msgid "You are no longer in god mode." msgstr "" @@ -7114,7 +7106,7 @@ msgstr "" msgid "You are now being annoyed." msgstr "" -#: ../../TShockAPI/Commands.cs:6757 +#: ../../TShockAPI/Commands.cs:6753 #, csharp-format msgid "You are now in god mode." msgstr "" @@ -7132,7 +7124,7 @@ msgid_plural "You butchered {0} NPCs." msgstr[0] "" msgstr[1] "" -#: ../../TShockAPI/TShock.cs:450 +#: ../../TShockAPI/TShock.cs:481 msgid "You can modify & distribute it under the terms of the GNU GPLv3." msgstr "" @@ -7210,7 +7202,7 @@ msgstr "" msgid "You cannot spawn banned items." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3445 +#: ../../TShockAPI/GetDataHandlers.cs:3486 msgid "You cannot use the Enchanted Sundial because time is stopped." msgstr "" @@ -7243,7 +7235,7 @@ msgstr[1] "" msgid "You didn't put a player name." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4211 +#: ../../TShockAPI/GetDataHandlers.cs:4256 msgid "You died! Normally, you'd be banned." msgstr "" @@ -7252,15 +7244,15 @@ msgstr "" msgid "You do not have access to this command." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:828 +#: ../../TShockAPI/TSPlayer.cs:829 msgid "You do not have permission to build in the spawn point." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:831 +#: ../../TShockAPI/TSPlayer.cs:832 msgid "You do not have permission to build in this region." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:825 +#: ../../TShockAPI/TSPlayer.cs:826 msgid "You do not have permission to build on this server." msgstr "" @@ -7296,7 +7288,7 @@ msgstr "" msgid "You do not have permission to grow this tree type" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2960 +#: ../../TShockAPI/GetDataHandlers.cs:2999 msgid "You do not have permission to hurt Town NPCs." msgstr "" @@ -7357,7 +7349,7 @@ msgstr "" msgid "You do not have permission to place actuators." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3967 +#: ../../TShockAPI/GetDataHandlers.cs:4011 msgid "You do not have permission to place Logic Sensors." msgstr "" @@ -7366,7 +7358,7 @@ msgstr "" msgid "You do not have permission to place this tile." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3493 +#: ../../TShockAPI/GetDataHandlers.cs:3534 msgid "You do not have permission to relocate Town NPCs." msgstr "" @@ -7382,15 +7374,15 @@ msgstr "" msgid "You do not have permission to send emotes!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3532 +#: ../../TShockAPI/GetDataHandlers.cs:3573 msgid "You do not have permission to spawn pets." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4127 +#: ../../TShockAPI/GetDataHandlers.cs:4171 msgid "You do not have permission to start a party." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3525 +#: ../../TShockAPI/GetDataHandlers.cs:3566 msgid "You do not have permission to start invasions." msgstr "" @@ -7399,19 +7391,19 @@ msgstr "" msgid "You do not have permission to start the {0} event." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4145 +#: ../../TShockAPI/GetDataHandlers.cs:4189 msgid "You do not have permission to start the Old One's Army." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3518 +#: ../../TShockAPI/GetDataHandlers.cs:3559 msgid "You do not have permission to summon bosses." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2970 +#: ../../TShockAPI/GetDataHandlers.cs:3009 msgid "You do not have permission to summon the Empress of Light." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2987 +#: ../../TShockAPI/GetDataHandlers.cs:3026 msgid "You do not have permission to summon the Lunatic Cultist!" msgstr "" @@ -7427,12 +7419,12 @@ msgstr "" msgid "You do not have permission to teleport other players." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3833 +#: ../../TShockAPI/GetDataHandlers.cs:3876 #, csharp-format msgid "You do not have permission to teleport using {0}." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3745 +#: ../../TShockAPI/GetDataHandlers.cs:3788 msgid "You do not have permission to teleport using items." msgstr "" @@ -7440,7 +7432,7 @@ msgstr "" msgid "You do not have permission to teleport using pylons." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3769 +#: ../../TShockAPI/GetDataHandlers.cs:3812 msgid "You do not have permission to teleport using Wormhole Potions." msgstr "" @@ -7458,7 +7450,7 @@ msgid "" "server-side-character data." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3437 +#: ../../TShockAPI/GetDataHandlers.cs:3478 msgid "You do not have permission to use the Enchanted Sundial." msgstr "" @@ -7473,7 +7465,7 @@ msgstr "" msgid "You failed to change your password." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2458 +#: ../../TShockAPI/GetDataHandlers.cs:2493 msgid "You have been Bounced." msgstr "" @@ -7504,7 +7496,7 @@ msgstr "" msgid "You have changed {0}'s group to {1} for {2}" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:4226 +#: ../../TShockAPI/GetDataHandlers.cs:4271 msgid "" "You have fallen in hardcore mode, and your items have been lost forever." msgstr "" @@ -7573,11 +7565,11 @@ msgstr "" msgid "You launched fireworks on yourself." msgstr "" -#: ../../TShockAPI/TShock.cs:608 +#: ../../TShockAPI/TShock.cs:648 msgid "You logged in from another location." msgstr "" -#: ../../TShockAPI/TShock.cs:599 +#: ../../TShockAPI/TShock.cs:639 msgid "You logged in from the same IP." msgstr "" @@ -7585,7 +7577,7 @@ msgstr "" msgid "You may now receive whispers from other players." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2621 +#: ../../TShockAPI/GetDataHandlers.cs:2656 msgid "" "You may wish to consider removing the tshock.ignore.ssc permission or " "negating it for this player." @@ -7599,7 +7591,7 @@ msgstr "" msgid "You must provide a setup code!" msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3448 +#: ../../TShockAPI/GetDataHandlers.cs:3489 msgid "" "You must set ForceTime to normal via config to use the Enchanted Sundial." msgstr "" @@ -7612,15 +7604,15 @@ msgstr "" msgid "You must use this command in-game." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2523 +#: ../../TShockAPI/GetDataHandlers.cs:2558 msgid "You need to join with a hardcore player." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2517 +#: ../../TShockAPI/GetDataHandlers.cs:2552 msgid "You need to join with a mediumcore player or higher." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:2511 +#: ../../TShockAPI/GetDataHandlers.cs:2546 msgid "You need to join with a softcore player." msgstr "" @@ -7648,7 +7640,7 @@ msgstr "" msgid "You were teleported to {0}." msgstr "" -#: ../../TShockAPI/TShock.cs:1706 +#: ../../TShockAPI/TShock.cs:1742 msgid "You will be teleported to your last known location..." msgstr "" @@ -7684,7 +7676,7 @@ msgstr "" msgid "Your client sent a blank character name." msgstr "" -#: ../../TShockAPI/TShock.cs:1351 +#: ../../TShockAPI/TShock.cs:1387 msgid "" "Your client sent a blank UUID. Configure it to send one or use a different " "client." @@ -7721,7 +7713,7 @@ msgid "" "off." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3217 +#: ../../TShockAPI/GetDataHandlers.cs:3257 msgid "Your password did not match this character's password." msgstr "" @@ -7742,7 +7734,7 @@ msgstr "" msgid "Your server-side character data has been saved." msgstr "" -#: ../../TShockAPI/TSPlayer.cs:1325 +#: ../../TShockAPI/TSPlayer.cs:1342 msgid "Your temporary group access has expired." msgstr "" @@ -7750,7 +7742,7 @@ msgstr "" msgid "z <#> - Sets the z-order of the region." msgstr "" -#: ../../TShockAPI/GetDataHandlers.cs:3235 +#: ../../TShockAPI/GetDataHandlers.cs:3275 msgctxt "Likely non-vanilla client send zero-length password" msgid "You have been Bounced for invalid password." msgstr "" From 60599ef5b58ef8f548ec36742a091f11ddda9d8b Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Fri, 22 Sep 2023 22:30:18 +0200 Subject: [PATCH 14/27] Rewrite the `.dockerignore` file into a denylist This should help with not forgetting to add any new directories (such as TShockInstaller and TShockPluginManager, which have been missing until now). --- .dockerignore | 33 +++++++++++++++++++-------------- docs/changelog.md | 1 + 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.dockerignore b/.dockerignore index b20ac5b68..234117c02 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,20 @@ -# ignore every file -* +# Ignore all Git metadata and build output (in all directories). +**/.git* +**/bin/ +**/obj/ -# except for the ones required for building -!i18n/ -!prebuilts/ -!TerrariaServerAPI/ -!TShockAPI/ -!TShockLauncher/ -!TShockLauncher.Tests/ -!TShock.sln - -# but exclude build artifacts -*/bin/ -*/obj/ +# Ignore other specific files that aren't needed for the build itself. +/.all-contributorsrc +/.dockerignore +/.editorconfig +/.vscode +/appveyor.yml +/COPYING +/crowdin.yml +/Dockerfile +/docs +/README.md +/README_cn.md +/renovate.json +/scripts +/SECURITY.md diff --git a/docs/changelog.md b/docs/changelog.md index 6a159e8a5..035df8ada 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -89,6 +89,7 @@ Use past tense when adding new entries; sign your name off when you add or chang * Added a method `TSPlayer.GiveItem`, which has `TShockAPI.NetItem` structure in its arguments. (@AgaSpace) * Added a property `TSPlayer.Hostile`, which gets pvp player mode. (@AgaSpace) * Fixed typo in `/gbuff`. (@sgkoishi, #2955) +* Rewrote the `.dockerignore` file into a denylist. (@timschumi) ## TShock 5.2 * An additional option `pvpwithnoteam` is added at `PvPMode` to enable PVP with no team. (@CelestialAnarchy, #2617, @ATFGK) From 93749be8bf57520d764264dbef007252883fe9a6 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Fri, 29 Sep 2023 17:36:58 +0200 Subject: [PATCH 15/27] Clear default Dockerfile assignments for `*PLATFORM` These kept Docker buildx from automatically building for multiple platforms. --- Dockerfile | 10 ++++++---- docs/docker.md | 8 ++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 186bee70e..87024fb9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,8 @@ -ARG TARGETPLATFORM=linux/amd64 -ARG BUILDPLATFORM=${TARGETPLATFORM} +# TARGETPLATFORM and BUILDPLATFORM are automatically filled in by Docker buildx. +# They should not be set in the global scope manually. FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:6.0 AS builder -ARG TARGETPLATFORM - # Copy build context WORKDIR /TShock COPY . ./ @@ -12,6 +10,10 @@ COPY . ./ # Build and package release based on target architecture RUN dotnet build -v m WORKDIR /TShock/TShockLauncher + +# Make TARGETPLATFORM available to the container. +ARG TARGETPLATFORM + RUN \ case "${TARGETPLATFORM}" in \ "linux/amd64") export ARCH="linux-x64" \ diff --git a/docs/docker.md b/docs/docker.md index 0b89681bb..ca0185103 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -14,15 +14,15 @@ Open ports can also be passed through using `-p :`. For Example: ```bash -# Building the image -docker build -t tshock:linux-amd64 --build-arg TARGETPLATFORM=linux/amd64 . +# Building the image using buildx and loading it into docker +docker buildx build -t tshock:latest --load . # Running the image docker run -p 7777:7777 -p 7878:7878 \ -v /home/cider/tshock/:/tshock \ -v /home/cider/.local/share/Terraria/Worlds:/worlds \ -v /home/cider/tshock/plugins:/plugins \ - --rm -it tshock:linux-amd64 \ + --rm -it tshock:latest \ -world /worlds/backflip.wld -motd "OMFG DOCKER" ``` @@ -33,7 +33,7 @@ Using `docker buildx`, you could build [multi-platform images](https://docs.dock For Example: ```bash # Building the image using buildx and loading it into docker -sudo docker buildx build -t tshock:linux-arm64 --platform linux/arm64 --load . +docker buildx build -t tshock:linux-arm64 --platform linux/arm64 --load . # Running the image docker run -p 7777:7777 -p 7878:7878 \ From 3ffe8e1274597151dee42a42f8df64a2f7b9a076 Mon Sep 17 00:00:00 2001 From: Tim Schumacher Date: Fri, 29 Sep 2023 17:13:00 +0200 Subject: [PATCH 16/27] Add CI for Docker images --- .github/workflows/ci-docker.yml | 25 +++++++++++++++++++++++++ docs/changelog.md | 1 + 2 files changed, 26 insertions(+) create mode 100644 .github/workflows/ci-docker.yml diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml new file mode 100644 index 000000000..deb811a2b --- /dev/null +++ b/.github/workflows/ci-docker.yml @@ -0,0 +1,25 @@ +name: CI (Docker image) + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: 'recursive' + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up buildx + uses: docker/setup-buildx-action@v3 + - name: Build image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/arm/v7,windows/amd64 + push: false + pull: true + cache-from: type=gha, scope=${{ github.workflow }} + cache-to: type=gha, scope=${{ github.workflow }} diff --git a/docs/changelog.md b/docs/changelog.md index 035df8ada..3cb52f80a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -90,6 +90,7 @@ Use past tense when adding new entries; sign your name off when you add or chang * Added a property `TSPlayer.Hostile`, which gets pvp player mode. (@AgaSpace) * Fixed typo in `/gbuff`. (@sgkoishi, #2955) * Rewrote the `.dockerignore` file into a denylist. (@timschumi) +* Added CI for Docker images. (@timschumi) ## TShock 5.2 * An additional option `pvpwithnoteam` is added at `PvPMode` to enable PVP with no team. (@CelestialAnarchy, #2617, @ATFGK) From a19ac72e0db7f37728f1ebaa1c4aaa4713642357 Mon Sep 17 00:00:00 2001 From: ZakFahey Date: Sun, 18 Feb 2024 11:36:26 -0800 Subject: [PATCH 17/27] Lazy initialize TextLog._logWriter There is an instance of this in SqlLog that's used as a backup, but since the StreamWriter is created in the constructor, the file is created too. This means that you'll get an empty text log created, every time a server starts up, even if you have text logs disabled. This is especially problematic when you have a multi-server setup with file write conflicts. --- TShockAPI/TextLog.cs | 14 +++++++++++--- docs/changelog.md | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/TShockAPI/TextLog.cs b/TShockAPI/TextLog.cs index 070c6c888..ae3bb29e5 100644 --- a/TShockAPI/TextLog.cs +++ b/TShockAPI/TextLog.cs @@ -29,7 +29,8 @@ namespace TShockAPI /// public class TextLog : ILog, IDisposable { - private readonly StreamWriter _logWriter; + private readonly bool ClearFile; + private StreamWriter _logWriter; /// /// File name of the Text log @@ -44,7 +45,7 @@ public class TextLog : ILog, IDisposable public TextLog(string filename, bool clear) { FileName = filename; - _logWriter = new StreamWriter(filename, !clear); + ClearFile = clear; } public bool MayWriteType(TraceLevel type) @@ -247,6 +248,10 @@ public void Write(string message, TraceLevel level) { if (!MayWriteType(level)) return; + if (_logWriter is null) + { + _logWriter = new StreamWriter(FileName, !ClearFile); + } var caller = "TShock"; @@ -278,7 +283,10 @@ public void Write(string message, TraceLevel level) public void Dispose() { - _logWriter.Dispose(); + if (_logWriter != null) + { + _logWriter.Dispose(); + } } } } diff --git a/docs/changelog.md b/docs/changelog.md index 035df8ada..ebb335cc3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -88,6 +88,7 @@ Use past tense when adding new entries; sign your name off when you add or chang * Added a method `TSPlayer.UpdateSection` with arguments `rectangle` and `isLoaded`, which will load some area from the server to the player. (@AgaSpace) * Added a method `TSPlayer.GiveItem`, which has `TShockAPI.NetItem` structure in its arguments. (@AgaSpace) * Added a property `TSPlayer.Hostile`, which gets pvp player mode. (@AgaSpace) +* Fixed bug where when the `UseSqlLogs` config property is true, an empty log file would still get created. (@ZakFahey) * Fixed typo in `/gbuff`. (@sgkoishi, #2955) * Rewrote the `.dockerignore` file into a denylist. (@timschumi) From 5c4db00f1e997bce7122f9da3937560d77ddfc08 Mon Sep 17 00:00:00 2001 From: Arthri <41360489+a@users.noreply.github.com> Date: Wed, 24 Apr 2024 05:31:49 +0000 Subject: [PATCH 18/27] Fix Cursed Flare --- TShockAPI/Bouncer.cs | 2 +- docs/changelog.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs index 6ff7fd1aa..58c14b1cd 100644 --- a/TShockAPI/Bouncer.cs +++ b/TShockAPI/Bouncer.cs @@ -2854,7 +2854,7 @@ internal static int GetMaxPlaceStyle(int tileID) { BuffID.Poisoned, 3600 }, // BuffID: 20 { BuffID.OnFire, 1200 }, // BuffID: 24 { BuffID.Confused, short.MaxValue }, // BuffID: 31 Brain of Confusion Internal Item ID: 3223 - { BuffID.CursedInferno, 420 }, // BuffID: 39 + { BuffID.CursedInferno, 600 }, // BuffID: 39 { BuffID.Frostburn, 900 }, // BuffID: 44 { BuffID.Ichor, 1200 }, // BuffID: 69 { BuffID.Venom, 1800 }, // BuffID: 70 diff --git a/docs/changelog.md b/docs/changelog.md index 27e037d61..b19745463 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -92,6 +92,7 @@ Use past tense when adding new entries; sign your name off when you add or chang * Fixed typo in `/gbuff`. (@sgkoishi, #2955) * Rewrote the `.dockerignore` file into a denylist. (@timschumi) * Added CI for Docker images. (@timschumi) +* Fixed Cursed Flares kicking players for invalid buff. (@Arthri) ## TShock 5.2 * An additional option `pvpwithnoteam` is added at `PvPMode` to enable PVP with no team. (@CelestialAnarchy, #2617, @ATFGK) From 5075997264b48e27960e3446a948ecb0ea0f5a03 Mon Sep 17 00:00:00 2001 From: quicm <2648373+quicm@users.noreply.github.com> Date: Tue, 17 Dec 2024 01:13:01 +1030 Subject: [PATCH 19/27] Add bandaid fix to RemoteClient.Reset for SecAd GHSA-hvm9-wc8j-mgrc --- TShockAPI/TShock.cs | 8 ++++++++ TShockAPI/TShockAPI.csproj | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index 054e31dfa..bcd72aa36 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -428,6 +428,8 @@ public override void Initialize() Hooks.AccountHooks.AccountDelete += OnAccountDelete; Hooks.AccountHooks.AccountCreate += OnAccountCreate; + On.Terraria.RemoteClient.Reset += RemoteClient_Reset; + GetDataHandlers.InitGetDataHandler(); Commands.InitCommands(); @@ -496,6 +498,12 @@ void SafeError(string message) } } + private static void RemoteClient_Reset(On.Terraria.RemoteClient.orig_Reset orig, RemoteClient client) + { + client.ClientUUID = null; + orig(client); + } + private static void OnAchievementInitializerLoad(ILContext il) { // Modify AchievementInitializer.Load to remove the Main.netMode == 2 check (occupies the first 4 IL instructions) diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index a37317092..9da8cd085 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -18,7 +18,7 @@ Also, be sure to release on github with the exact assembly version tag as below so that the update manager works correctly (via the Github releases api and mimic) --> - 5.2.0 + 5.2.1 TShock for Terraria Pryaxis & TShock Contributors TShockAPI From 3da4bf4b45ad77f96afb261aaf92c82056c7bf4c Mon Sep 17 00:00:00 2001 From: Lucas Nicodemus Date: Fri, 3 Jan 2025 00:05:25 +0900 Subject: [PATCH 20/27] Remove inactive people from sponsors --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 8275735a2..75b69d48c 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ # These are supported funding model platforms -github: [SignatureBeef, hakusaro, Stealownz, QuiCM] +github: [SignatureBeef, QuiCM] From 99701f9a8415382c8a48ecc324d988a8a579d3f0 Mon Sep 17 00:00:00 2001 From: Lucas Nicodemus Date: Sat, 4 Jan 2025 05:31:04 +0900 Subject: [PATCH 21/27] Update logo url in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5559b37f..0369df531 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- TShock for Terraria
+ TShock for Terraria
AppVeyor Build Status From 1b31665a888769be8747637c5be51e81ff8cbf26 Mon Sep 17 00:00:00 2001 From: Lucas Nicodemus Date: Sat, 4 Jan 2025 05:31:27 +0900 Subject: [PATCH 22/27] Update logo in readme --- README_cn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_cn.md b/README_cn.md index e038817e4..65c5eba61 100644 --- a/README_cn.md +++ b/README_cn.md @@ -1,5 +1,5 @@

- TShock for Terraria
+ TShock for Terraria
AppVeyor Build Status From af969898b1e54f684e48cd30bc019393725c4a00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Jan 2025 21:17:29 +0000 Subject: [PATCH 23/27] Update dependency NuGet.Packaging to 6.3.4 [SECURITY] --- TShockPluginManager/TShockPluginManager.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TShockPluginManager/TShockPluginManager.csproj b/TShockPluginManager/TShockPluginManager.csproj index 33c503fde..774f98196 100644 --- a/TShockPluginManager/TShockPluginManager.csproj +++ b/TShockPluginManager/TShockPluginManager.csproj @@ -7,7 +7,7 @@ - + From d53517104ced20ce5f7b17b8351b200d20769463 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:45:58 +0000 Subject: [PATCH 24/27] Update dependency NuGet.Protocol to 6.3.3 [SECURITY] --- TShockPluginManager/TShockPluginManager.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TShockPluginManager/TShockPluginManager.csproj b/TShockPluginManager/TShockPluginManager.csproj index 774f98196..1d53f320b 100644 --- a/TShockPluginManager/TShockPluginManager.csproj +++ b/TShockPluginManager/TShockPluginManager.csproj @@ -8,7 +8,7 @@ - + From e21322b86beb0850d4302aa9c622d5c825ea3dec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:58:00 +0000 Subject: [PATCH 25/27] Update dependency SharpZipLib to 1.4.2 --- TShockInstaller/TShockInstaller.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TShockInstaller/TShockInstaller.csproj b/TShockInstaller/TShockInstaller.csproj index 9b73d66a1..ac3bfff40 100644 --- a/TShockInstaller/TShockInstaller.csproj +++ b/TShockInstaller/TShockInstaller.csproj @@ -12,6 +12,6 @@ - + From 78dd32a8374c9d8a0bc5b5a80b37c38708fb21f1 Mon Sep 17 00:00:00 2001 From: Lucas Nicodemus Date: Sat, 25 Jan 2025 21:28:24 +0900 Subject: [PATCH 26/27] Fix changelog versioning --- docs/changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index b19745463..54b23a8ab 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -78,6 +78,9 @@ Use past tense when adding new entries; sign your name off when you add or chang * If there is no section called "Upcoming changes" below this line, please add one with `## Upcoming changes` as the first line, and then a bulleted item directly after with the first change. --> ## Upcoming changes +* You know the drill + +## TShock 5.2.1 * Updated `TSPlayer.GodMode`. (@AgaSpace) * Previously the field was used as some kind of dataset changed by /godmode command, but now it is a property that receives/changes data in journey mode. * Added the `TSPlayer.Client` property. It allows the developer to get the `RemoteClient` player, without an additional call to `Terraria.Netplay.Clients`. (@AgaSpace) From b4968adb7d964f851633f840d407654e6e90b9aa Mon Sep 17 00:00:00 2001 From: Lucas Nicodemus Date: Sat, 25 Jan 2025 22:27:48 +0900 Subject: [PATCH 27/27] Fix typo in config file Closes https://github.com/Pryaxis/TShock/pull/3052 --- TShockAPI/Configuration/TShockConfig.cs | 4 ++-- docs/changelog.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/TShockAPI/Configuration/TShockConfig.cs b/TShockAPI/Configuration/TShockConfig.cs index 79c374b25..c91466fec 100644 --- a/TShockAPI/Configuration/TShockConfig.cs +++ b/TShockAPI/Configuration/TShockConfig.cs @@ -320,8 +320,8 @@ public class TShockSettings [Description("The reason given if banning a mediumcore player on death.")] public string MediumcoreBanReason = GetString("Death results in a ban"); - ///

Disbales IP bans by default, if no arguments are passed to the ban command. - [Description("Disbales IP bans by default, if no arguments are passed to the ban command.")] + /// Disables IP bans by default, if no arguments are passed to the ban command. + [Description("Disables IP bans by default, if no arguments are passed to the ban command.")] public bool DisableDefaultIPBan; /// Enable or disable the whitelist based on IP addresses in the whitelist.txt file. diff --git a/docs/changelog.md b/docs/changelog.md index 683cddcb7..00aec595e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -79,6 +79,8 @@ Use past tense when adding new entries; sign your name off when you add or chang ## Upcoming changes * Fixed `/dump-reference-data` mutate the command names. (#2943, @sgkoishi) +* You know the drill +* Fix typo in config for IP bans. (@redchess64) ## TShock 5.2.1 * Updated `TSPlayer.GodMode`. (@AgaSpace)