From b714ab18a548ff38fea565fbdb35a9ef24eb80e7 Mon Sep 17 00:00:00 2001 From: punchready Date: Tue, 4 Apr 2023 05:08:17 +0200 Subject: [PATCH 01/36] 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 6ff7fd1a..c7c72f96 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 @@ namespace TShockAPI /// 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 00000000..86048193 --- /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/36] Update changelog --- docs/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index c4a95d14..48c965d5 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/36] Update STR checks to be even more strict --- .../Handlers/SendTileRectHandlerRefactor.cs | 125 ++++++++++++------ 1 file changed, 85 insertions(+), 40 deletions(-) diff --git a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs index 86048193..9b8b0096 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -69,7 +69,7 @@ namespace TShockAPI.Handlers /// private readonly struct TileRectMatch { - public const short IGNORE_FRAME = -1; + private const short IGNORE_FRAME = -1; private enum MatchType { @@ -84,10 +84,12 @@ namespace TShockAPI.Handlers 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 @@ namespace TShockAPI.Handlers TileType = tileType; MaxFrameX = maxFrameX; MaxFrameY = maxFrameY; + FrameXStep = frameXStep; + FrameYStep = frameYStep; } /// @@ -105,10 +109,12 @@ namespace TShockAPI.Handlers /// 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 @@ namespace TShockAPI.Handlers /// 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 @@ namespace TShockAPI.Handlers /// 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 @@ namespace TShockAPI.Handlers { 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 @@ namespace TShockAPI.Handlers /// /// 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.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.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.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/36] 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 9b8b0096..a3c87100 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -107,8 +107,8 @@ namespace TShockAPI.Handlers /// 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 @@ namespace TShockAPI.Handlers { 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/36] 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 a3c87100..1472423f 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -416,6 +416,16 @@ namespace TShockAPI.Handlers // 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 @@ namespace TShockAPI.Handlers 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/36] 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 1472423f..b3a2d1e7 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -211,10 +211,14 @@ namespace TShockAPI.Handlers { 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/36] 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 b3a2d1e7..34f8a495 100644 --- a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs +++ b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs @@ -214,11 +214,11 @@ namespace TShockAPI.Handlers // 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 @@ namespace TShockAPI.Handlers /// - /// 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 @@ namespace TShockAPI.Handlers 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? + + if (!player.HasBuildPermission(rect.X, rect.Y)) { - // wallbans when? + // 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; - 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 true; } } - return matchedTileOrWall; + return false; } @@ -881,4 +848,483 @@ namespace TShockAPI.Handlers 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/36] Remove old STR handler --- TShockAPI/Bouncer.cs | 4 +- TShockAPI/Handlers/SendTileRectHandler.cs | 1858 +++++++++++------ .../Handlers/SendTileRectHandlerRefactor.cs | 1330 ------------ 3 files changed, 1226 insertions(+), 1966 deletions(-) delete mode 100644 TShockAPI/Handlers/SendTileRectHandlerRefactor.cs diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs index c7c72f96..6ff7fd1a 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 @@ namespace TShockAPI /// 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 80d5a47c..64d9d8a7 100644 --- a/TShockAPI/Handlers/SendTileRectHandler.cs +++ b/TShockAPI/Handlers/SendTileRectHandler.cs @@ -1,26 +1,644 @@ -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 + { + 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() { @@ -44,27 +662,19 @@ namespace TShockAPI.Handlers } }, }; - /// - /// 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` - /// - private static readonly Dictionary> FlowerBootPlantToStyleMap = new Dictionary>() + private static readonly Dictionary> GrassToStyleMap = new Dictionary>() { { 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() @@ -78,662 +688,642 @@ namespace TShockAPI.Handlers }; /// - /// Item IDs that can spawn flowers while you walk + /// Checks whether the tile rect is a valid Flower Boots placement. /// - public static List FlowerBootItems = new List + /// 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) { - ItemID.FlowerBoots, - ItemID.FairyBoots - }; - - /// - /// 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 } - }; - - /// - /// Invoked when a SendTileRect packet is received - /// - /// - /// - public void OnReceive(object sender, GetDataHandlers.SendTileRectEventArgs args) - { - // By default, we'll handle everything - args.Handled = true; - - if (ShouldSkipProcessing(args)) + if (rect.Width != 1 || rect.Height != 1) { - return; + return false; } - 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); - - IterateTileRect(tiles, processed, args); - - // 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); - - // 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) + if (!player.TPlayer.flowerBoots) { - TSPlayer.All.SendTileRect(args.TileX, args.TileY, args.Width, args.Length); - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect reimplemented from carbonara from {args.Player.Name}")); - } - } - - /// - /// 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; - - for (int x = 0; x < width; x++) - { - for (int y = 0; y < length; y++) - { - // Do not process already processed tiles - if (processed[x, y]) - { - continue; - } - - 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)) - { - processed[x, y] = true; - continue; - } - - // Do not process tiles that the player cannot update - if (!args.Player.HasBuildPermission(realX, realY) || - !args.Player.IsInRange(realX, realY)) - { - processed[x, y] = true; - continue; - } - - NetTile newTile = tiles[x, y]; - - TileObjectData data; - - // 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) - { - // 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) - { - 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; - } - } - } - else - { - // 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; - } - } - } - - 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)) - { - continue; - } - - newTiles = new NetTile[objWidth, objHeight]; - - for (int i = 0; i < objWidth; i++) - { - for (int j = 0; j < objHeight; j++) - { - newTiles[i, j] = tiles[x + i, y + j]; - processed[x + i, y + j] = true; - } - } - 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; - } - } - } - - /// - /// Processes a tile object consisting of multiple tiles from the tile rect packet - /// - /// 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) - { - // 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)) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from no permission for tile object from {args.Player.Name}")); - return; + return false; } - if (TShock.TileBans.TileIsBanned((short)tileType)) - { - TShock.Log.ConsoleDebug(GetString("Bouncer / SendTileRect rejected for banned tile")); - return; - } + ITile oldTile = Main.tile[rect.X, rect.Y]; + NetTile newTile = rect[0, 0]; - // Update all tiles in the tile object. These will be sent back to the player later - UpdateMultipleServerTileStates(realX, realY, width, height, newTiles); - - // Tile entities have special placements that we should let the game deal with - if (TileEntityIdToTileIdMap.ContainsKey(tileType)) - { - TileEntity.PlaceEntityNet(realX, realY, TileEntityIdToTileIdMap[tileType]); - } - } - - /// - /// 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))) - { - ProcessFlowerBoots(realX, realY, newTile); - return; - } - - ITile tile = Main.tile[realX, realY]; - - // Triggering a single land mine tile - if (rectWidth == 1 && rectLength == 1 && tile.type == TileID.LandMine && !newTile.Active) - { - UpdateServerTileState(tile, newTile, TileDataType.Tile); - } - - // Hammering a single junction box - if (rectWidth == 1 && rectLength == 1 && tile.type == TileID.WirePipe) - { - UpdateServerTileState(tile, newTile, TileDataType.Tile); - } - - // 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 - )) - { - 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 - { - WorldGen.KillTile(realX, realY + 1); - } - } - - // 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) - { - ProcessConversionSpreads(tile, newTile); - } - - // All other single tile updates should not be processed. - } - - /// - /// Applies changes to a tile if a tile rect for flower-growing boots is valid - /// - /// 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) - { - 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)) + 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)) ) { - UpdateServerTileState(tile, newTile, TileDataType.Tile); - } - } - - // 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(); - - /// - /// 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) - /// - /// The tile to update - /// The NetTile containing new tile properties - internal void ProcessConversionSpreads(ITile tile, NetTile newTile) - { - 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 - { - foreach (var tileType in _convertibleTiles) + if (TShock.TileBans.TileIsBanned((short)newTile.Type, player)) { - if (tileType[tile.type] && tileType[newTile.Type]) - { - allowTile = true; - break; - } + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; } - } - if (allowTile) - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect processing a tile conversion update - [{tile.type}] -> [{newTile.Type}]")); - UpdateServerTileState(tile, newTile, TileDataType.Tile); - } - - foreach (var wallType in _convertibleWalls) - { - if (wallType[tile.wall] && wallType[newTile.Wall]) + if (!player.HasBuildPermission(rect.X, rect.Y)) { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect processing a wall conversion update - [{tile.wall}] -> [{newTile.Wall}]")); - UpdateServerTileState(tile, newTile, TileDataType.Wall); - break; + // for simplicity, let's pretend that the edit was valid, but do not execute it + return true; } - } - } - /// - /// Updates a single tile's world state with a set of changes from the networked tile state - /// - /// 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) - { - //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. + 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; - if ((updateType & TileDataType.Tile) != 0) - { - tile.active(newTile.Active); - tile.type = newTile.Type; - - if (newTile.FrameImportant) - { - tile.frameX = newTile.FrameX; - tile.frameY = newTile.FrameY; - } - else if (tile.type != newTile.Type || !tile.active()) - { - //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; - } - } - - if ((updateType & TileDataType.Wall) != 0) - { - tile.wall = newTile.Wall; - } - - if ((updateType & TileDataType.TilePaint) != 0) - { - tile.color(newTile.TileColor); - tile.fullbrightBlock(newTile.FullbrightBlock); - tile.invisibleBlock(newTile.InvisibleBlock); - } - - if ((updateType & TileDataType.WallPaint) != 0) - { - tile.wallColor(newTile.WallColor); - tile.fullbrightWall(newTile.FullbrightWall); - tile.invisibleWall(newTile.InvisibleWall); - } - - if ((updateType & TileDataType.Liquid) != 0) - { - tile.liquid = newTile.Liquid; - tile.liquidType(newTile.LiquidType); - } - - if ((updateType & TileDataType.Slope) != 0) - { - tile.halfBrick(newTile.IsHalf); - tile.slope(newTile.Slope); - } - - if ((updateType & TileDataType.Wiring) != 0) - { - tile.wire(newTile.Wire); - tile.wire2(newTile.Wire2); - tile.wire3(newTile.Wire3); - tile.wire4(newTile.Wire4); - } - - if ((updateType & TileDataType.Actuator) != 0) - { - tile.actuator(newTile.IsActuator); - tile.inActive(newTile.Inactive); - } - } - - /// - /// Performs on multiple tiles - /// - /// - /// - /// - /// - /// - public static void UpdateMultipleServerTileStates(int x, int y, int width, int height, NetTile[,] newTiles) - { - for (int i = 0; i < width; i++) - { - for (int j = 0; j < height; j++) - { - UpdateServerTileState(Main.tile[x + i, y + j], newTiles[i, j], TileDataType.Tile); - } - } - } - - /// - /// Reads a set of NetTiles from a memory stream - /// - /// - /// - /// - /// - static NetTile[,] ReadNetTilesFromStream(System.IO.MemoryStream stream, byte width, byte length) - { - NetTile[,] tiles = new NetTile[width, length]; - for (int x = 0; x < width; x++) - { - for (int y = 0; y < length; y++) - { - tiles[x, y] = new NetTile(stream); - } - } - - return tiles; - } - - /// - /// Determines whether or not the tile rect should be immediately accepted or rejected - /// - /// - /// - static bool ShouldSkipProcessing(GetDataHandlers.SendTileRectEventArgs args) - { - 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; - } - - 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 - { - TShock.Log.ConsoleDebug(GetString($"Bouncer / SendTileRect rejected from non-vanilla tilemod from {args.Player.Name}")); - return true; - } - - if (args.Player.IsBouncerThrottled()) - { - 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; - } - - if (args.Player.IsBeingDisabled()) - { - 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; } return false; } - /// - /// Checks if a tile object fits inside the dimensions of a tile rectangle - /// - /// - /// - /// - /// - /// - /// - /// - /// - static bool DoesTileObjectFitInTileRect(int x, int y, int width, int height, short rectWidth, short rectLength, bool[,] processed) - { - 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; - } - } - TShock.Log.ConsoleDebug(GetString("Bouncer / SendTileRectHandler - rejected tile object because object dimensions fall outside the tile rect (excessive size)")); + 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; } - return true; + 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; } - class Debug + + /// + /// 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) { - /// - /// 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) + if (rect.Width != 1 || rect.Height != 1) { - if (TShock.Config.Settings.DebugLogs) + 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) { - char pad = '0'; - for (int y = 0; y < height; y++) - { - int realY = y + tileY; - for (int x = 0; x < width; x++) - { - 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" : "-") + " "); - } - Console.Write(" -> "); - for (int x = 0; x < width; x++) - { - 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" : "-") + " "); - } - Console.Write("\n"); - } + 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; } } - /// - /// 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) + public ushort wall { - for (int x = 0; x < width; x++) + get => _wall; + set { - 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); + _setWalls.Add(value); + _wall = value; } + } - player.SendTileRect(tileX, tileY, width, height); +#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; } } } diff --git a/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs b/TShockAPI/Handlers/SendTileRectHandlerRefactor.cs deleted file mode 100644 index 34f8a495..00000000 --- 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 6d4682ed7e37722054b23c885cd6c85987605ccc Mon Sep 17 00:00:00 2001 From: AkjaHAsLk1IALk0MasH <46046453+AgaSpace@users.noreply.github.com> Date: Sun, 14 May 2023 09:11:18 +0700 Subject: [PATCH 09/36] Updated the `GodMode` field. * Changed the GodMode field to a property that is controlled by the journey mode. - Removed the call to godmode in `Commands.ToggleGodMode`. --- TShockAPI/Commands.cs | 4 ---- TShockAPI/TSPlayer.cs | 11 +++++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index 2214f252..3d1767ea 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -6740,10 +6740,6 @@ namespace TShockAPI playerToGod.GodMode = !playerToGod.GodMode; - var godPower = CreativePowerManager.Instance.GetPower(); - - godPower.SetEnabledState(playerToGod.Index, playerToGod.GodMode); - if (playerToGod != args.Player) { args.Player.SendSuccessMessage(playerToGod.GodMode diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index ef5c3c22..f6ece337 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -34,6 +34,7 @@ using TShockAPI.Hooks; using TShockAPI.Net; using Timer = System.Timers.Timer; using System.Linq; +using Terraria.GameContent.Creative; namespace TShockAPI { @@ -935,9 +936,15 @@ namespace TShockAPI public bool LoginHarassed = false; /// - /// Player cant die, unless onehit + /// Controls the journey godmode /// - public bool GodMode = false; + public bool GodMode + { + get => + CreativePowerManager.Instance.GetPower().IsEnabledForPlayer(Index); + set => + CreativePowerManager.Instance.GetPower().SetEnabledState(Index, value); + } /// /// Players controls are inverted if using SSC From c047b0e87d5309b503c9d0beb91797a81dafe38f Mon Sep 17 00:00:00 2001 From: AkjaHAsLk1IALk0MasH <46046453+AgaSpace@users.noreply.github.com> Date: Sun, 14 May 2023 09:14:43 +0700 Subject: [PATCH 10/36] Added the `Client` property. The `Client` property gets data from the array `Terraria.Netplay.Clients`. --- TShockAPI/TSPlayer.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index f6ece337..10b2e760 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -988,7 +988,7 @@ namespace TShockAPI get { return RealPlayer - && (Netplay.Clients[Index] != null && Netplay.Clients[Index].IsActive && !Netplay.Clients[Index].PendingTermination); + && (Client != null && Client.IsActive && !Client.PendingTermination); } } @@ -1005,8 +1005,8 @@ namespace TShockAPI /// public int State { - get { return Netplay.Clients[Index].State; } - set { Netplay.Clients[Index].State = value; } + get { return Client.State; } + set { Client.State = value; } } /// @@ -1014,7 +1014,7 @@ namespace TShockAPI /// public string UUID { - get { return RealPlayer ? Netplay.Clients[Index].ClientUUID : ""; } + get { return RealPlayer ? Client.ClientUUID : ""; } } /// @@ -1026,8 +1026,8 @@ namespace TShockAPI { if (string.IsNullOrEmpty(CacheIP)) return - CacheIP = RealPlayer ? (Netplay.Clients[Index].Socket.IsConnected() - ? TShock.Utils.GetRealIP(Netplay.Clients[Index].Socket.GetRemoteAddress().ToString()) + CacheIP = RealPlayer ? (Client.Socket.IsConnected() + ? TShock.Utils.GetRealIP(Client.Socket.GetRemoteAddress().ToString()) : "") : "127.0.0.1"; else @@ -1110,6 +1110,11 @@ namespace TShockAPI } + /// + /// Player RemoteClient. + /// + public RemoteClient Client => Netplay.Clients[Index]; + /// /// Gets the Terraria Player object associated with the player. /// @@ -2071,7 +2076,7 @@ namespace TShockAPI if (!RealPlayer || !ConnectionAlive) return; - Netplay.Clients[Index].Socket.AsyncSend(data, 0, data.Length, Netplay.Clients[Index].ServerWriteCallBack); + Client.Socket.AsyncSend(data, 0, data.Length, Client.ServerWriteCallBack); } /// From fa9e4419bd6ca6830a1b7684dfff168e549b0420 Mon Sep 17 00:00:00 2001 From: AkjaHAsLk1IALk0MasH <46046453+AgaSpace@users.noreply.github.com> Date: Sun, 14 May 2023 09:16:14 +0700 Subject: [PATCH 11/36] Added documentation to the `withMsg` parameter in the `SetPvP` method. --- TShockAPI/TSPlayer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index 10b2e760..f4cd0a3d 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -1852,6 +1852,7 @@ namespace TShockAPI /// Sets the player's pvp. /// /// The state of the pvp mode. + /// Whether a chat message about the change should be sent. public virtual void SetPvP(bool mode, bool withMsg = false) { Main.player[Index].hostile = mode; From 3647bbaf54d16ceead0a0ab96172d113f1d20d93 Mon Sep 17 00:00:00 2001 From: AkjaHAsLk1IALk0MasH <46046453+AgaSpace@users.noreply.github.com> Date: Sun, 14 May 2023 09:20:20 +0700 Subject: [PATCH 12/36] Added a reason for the `KillPlayer` and `DamagePlayer` methods. Added overloads that have a cause in the parameters (`PlayerDeathReason`) --- TShockAPI/TSPlayer.cs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index f4cd0a3d..6bfe0c44 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -1827,7 +1827,17 @@ namespace TShockAPI /// The amount of damage the player will take. public virtual void DamagePlayer(int damage) { - NetMessage.SendPlayerHurt(Index, PlayerDeathReason.LegacyDefault(), damage, (new Random()).Next(-1, 1), false, false, 0, -1, -1); + DamagePlayer(damage, PlayerDeathReason.LegacyDefault()); + } + + /// + /// Wounds the player with the given damage. + /// + /// The amount of damage the player will take. + /// The reason for causing damage to player. + public virtual void DamagePlayer(int damage, PlayerDeathReason reason) + { + NetMessage.SendPlayerHurt(Index, reason, damage, (new Random()).Next(-1, 1), false, false, 0, -1, -1); } /// @@ -1835,7 +1845,16 @@ namespace TShockAPI /// public virtual void KillPlayer() { - NetMessage.SendPlayerDeath(Index, PlayerDeathReason.LegacyDefault(), 99999, (new Random()).Next(-1, 1), false, -1, -1); + KillPlayer(PlayerDeathReason.LegacyDefault()); + } + + /// + /// Kills the player. + /// + /// Reason for killing a player. + public virtual void KillPlayer(PlayerDeathReason reason) + { + NetMessage.SendPlayerDeath(Index, reason, 99999, (new Random()).Next(-1, 1), false, -1, -1); } /// From d9352d690240ad34d142fae30b2e37c26e2e9b86 Mon Sep 17 00:00:00 2001 From: AkjaHAsLk1IALk0MasH <46046453+AgaSpace@users.noreply.github.com> Date: Sun, 14 May 2023 09:23:42 +0700 Subject: [PATCH 13/36] Added an exception that occurs when the developer changes team. --- TShockAPI/TSPlayer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index 6bfe0c44..284d9a31 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -1863,6 +1863,8 @@ namespace TShockAPI /// The team color index. public virtual void SetTeam(int team) { + if (team < 0 || team >= Main.teamColor.Length) + throw new ArgumentException("The player's team is not in the range of available."); Main.player[Index].team = team; NetMessage.SendData((int)PacketTypes.PlayerTeam, -1, -1, NetworkText.Empty, Index); } From 3d585d4d699d74c0f3f4cd569e00f7fae9f5324a Mon Sep 17 00:00:00 2001 From: AkjaHAsLk1IALk0MasH <46046453+AgaSpace@users.noreply.github.com> Date: Sun, 14 May 2023 09:38:35 +0700 Subject: [PATCH 14/36] Added the `UpdateSection` method. I described its action in the comments. --- TShockAPI/TSPlayer.cs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index 284d9a31..79572412 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -1499,6 +1499,41 @@ namespace TShockAPI return false; } + /// + /// Changes the values of the array. + /// + /// The area of the sections you want to set a value to. + /// The minimum size should be set to 200x150. If null, then the entire map is specified. + /// Is the section loaded. + // The server does not send the player the whole world, it sends it in sections. To do this, it sets up visible and invisible sections. + // If the player was not in any section(Client.TileSections[x, y] == false) then the server will send the missing section of the world. + // This method allows you to simulate what the player has or has not seen these sections. + // For example, we can put some number of earths blocks in some vast area, for example, for the whole world, but the player will not see the changes, because some section is already loaded for him. At this point this method can come into effect! With it we will be able to select some zone and make it both visible and invisible to the player. + // The server will assume that the zone is not loaded on the player, and will resend the data, but with earth blocks. + public void UpdateSection(Rectangle? rectangle = null, bool isLoaded = false) + { + if (rectangle.HasValue) + { + for (int i = Netplay.GetSectionX(rectangle.Value.X); i < Netplay.GetSectionX(rectangle.Value.X + rectangle.Value.Width) && i < Main.maxSectionsX; i++) + { + for (int j = Netplay.GetSectionY(rectangle.Value.Y); j < Netplay.GetSectionY(rectangle.Value.Y + rectangle.Value.Height) && j < Main.maxSectionsY; j++) + { + Client.TileSections[i, j] = isLoaded; + } + } + } + else + { + for (int i = 0; i < Main.maxSectionsX; i++) + { + for (int j = 0; j < Main.maxSectionsY; j++) + { + Client.TileSections[i, j] = isLoaded; + } + } + } + } + /// /// Gives an item to the player. Includes banned item spawn prevention to check if the player can spawn the item. /// From 2f7514c0efc1c9411e7346bdb00852935c4a0533 Mon Sep 17 00:00:00 2001 From: AkjaHAsLk1IALk0MasH <46046453+AgaSpace@users.noreply.github.com> Date: Sun, 14 May 2023 09:43:18 +0700 Subject: [PATCH 15/36] Added an overload for `TSPlayer.GiveItem` Added `TShockAPI.NetItem` structure to the parameters. --- TShockAPI/TSPlayer.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index 79572412..8aadacdf 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -1566,6 +1566,15 @@ namespace TShockAPI GiveItemByDrop(type, stack, prefix); } + /// + /// Gives an item to the player. + /// + /// Item with data to be given to the player. + public virtual void GiveItem(NetItem item) + { + GiveItem(item.NetId, item.Stack, item.PrefixId); + } + private Item EmptySentinelItem = new Item(); private bool Depleted(Item item) From 4d92f11cc805a52906abf974c6caf043035f5b4c Mon Sep 17 00:00:00 2001 From: AkjaHAsLk1IALk0MasH <46046453+AgaSpace@users.noreply.github.com> Date: Sun, 14 May 2023 09:56:47 +0700 Subject: [PATCH 16/36] Added the `TSPlayer.Hostile` property. It gets `TPlayer.hostile`. I also wanted to add the ability to change `TPlayer.hostile`, but noticed a property `TSPlayer.Team`. You can only use the `TSPlayer.SetTeam` method to change it. So it's exactly the same here: You can use the method `TSPlayer.SetPvP` (By the way, it should be renamed to `TSPlayer.SetHostile`) --- TShockAPI/TSPlayer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index 8aadacdf..c9194c36 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -1147,6 +1147,11 @@ namespace TShockAPI get { return TPlayer.team; } } + /// + /// Gets PvP player mode. + /// + public bool Hostile => TPlayer.hostile; + /// /// Gets the player's X coordinate. /// From b30a8cb6b2c4f1d5fff8b12e1ce58c01e70c57b9 Mon Sep 17 00:00:00 2001 From: AkjaHAsLk1IALk0MasH <46046453+AgaSpace@users.noreply.github.com> Date: Sun, 14 May 2023 10:03:55 +0700 Subject: [PATCH 17/36] Update changelog.md --- docs/changelog.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 3f64f355..f02867f8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -78,7 +78,16 @@ 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! +* 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) +* Updated the documentation for the `TSPlayer.SetPvP` method. The `sendMsg` parameter, which is responsible for sending a pvp mode change message, was not documented earlier. (@AgaSpace) +* Added methods `TSPlayer.KillPlayer` and `TSPlayer.DamagePlayer` for which you can specify the cause (`PlayerDeathReason`) in parameters. +* Added an error when trying to change a `TSPlayer` team to, say, 9, when there are only 6. (@AgaSpace) +* Added an error when trying to call the `TSPlayer.SetTeam` method with an argument (team) greater than 5 or less than 0. (@AgaSpace) +* 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. ## TShock 5.2 * An additional option `pvpwithnoteam` is added at `PvPMode` to enable PVP with no team. (@CelestialAnarchy, #2617, @ATFGK) 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 18/36] 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 64d9d8a7..3aa4b14c 100644 --- a/TShockAPI/Handlers/SendTileRectHandler.cs +++ b/TShockAPI/Handlers/SendTileRectHandler.cs @@ -266,7 +266,7 @@ namespace TShockAPI.Handlers { 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 19/36] 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 3aa4b14c..d85185b8 100644 --- a/TShockAPI/Handlers/SendTileRectHandler.cs +++ b/TShockAPI/Handlers/SendTileRectHandler.cs @@ -506,11 +506,7 @@ namespace TShockAPI.Handlers } // 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 72ecc472187dde708ea2a2d1176e3b9872cd27f7 Mon Sep 17 00:00:00 2001 From: James Puleo Date: Fri, 9 Jun 2023 05:52:19 -0400 Subject: [PATCH 20/36] Update `docs/changelog.md` (#2953) --- docs/changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index f02867f8..89e4d39c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -82,12 +82,12 @@ Use past tense when adding new entries; sign your name off when you add or chang * 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) * Updated the documentation for the `TSPlayer.SetPvP` method. The `sendMsg` parameter, which is responsible for sending a pvp mode change message, was not documented earlier. (@AgaSpace) -* Added methods `TSPlayer.KillPlayer` and `TSPlayer.DamagePlayer` for which you can specify the cause (`PlayerDeathReason`) in parameters. +* Added methods `TSPlayer.KillPlayer` and `TSPlayer.DamagePlayer` for which you can specify the cause (`PlayerDeathReason`) in parameters. (@AgaSpace) * Added an error when trying to change a `TSPlayer` team to, say, 9, when there are only 6. (@AgaSpace) * Added an error when trying to call the `TSPlayer.SetTeam` method with an argument (team) greater than 5 or less than 0. (@AgaSpace) * 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. +* Added a property `TSPlayer.Hostile`, which gets pvp player mode. (@AgaSpace) ## TShock 5.2 * An additional option `pvpwithnoteam` is added at `PvPMode` to enable PVP with no team. (@CelestialAnarchy, #2617, @ATFGK) From 5bcde689d4262ae18ca639e4a9895d078ba31cc4 Mon Sep 17 00:00:00 2001 From: SGKoishi Date: Sun, 11 Jun 2023 22:11:20 -0700 Subject: [PATCH 21/36] Fix a typo in gbuff --- TShockAPI/Commands.cs | 2 +- docs/changelog.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index 3d1767ea..a4669b04 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -6462,7 +6462,7 @@ namespace TShockAPI if (target == user) user.SendSuccessMessage(GetString($"You buffed yourself with {TShock.Utils.GetBuffName(id)} ({TShock.Utils.GetBuffDescription(id)}) for {time} seconds.")); else - target.SendSuccessMessage(GetString($"You have buffed {user.Name} with {TShock.Utils.GetBuffName(id)} ({TShock.Utils.GetBuffDescription(id)}) for {time} seconds!")); + user.SendSuccessMessage(GetString($"You have buffed {user.Name} with {TShock.Utils.GetBuffName(id)} ({TShock.Utils.GetBuffDescription(id)}) for {time} seconds!")); if (!args.Silent && target != user) target.SendSuccessMessage(GetString($"{user.Name} has buffed you with {TShock.Utils.GetBuffName(id)} ({TShock.Utils.GetBuffDescription(id)}) for {time} seconds!")); } diff --git a/docs/changelog.md b/docs/changelog.md index 89e4d39c..d4ab3cfe 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 typo in `/gbuff`. (@sgkoishi, #2955) ## TShock 5.2 * An additional option `pvpwithnoteam` is added at `PvPMode` to enable PVP with no team. (@CelestialAnarchy, #2617, @ATFGK) From f66ad72e2caeec3de8c6a64cc007afdc83d1637a Mon Sep 17 00:00:00 2001 From: Stargazing Koishi Date: Sun, 11 Jun 2023 22:20:48 -0700 Subject: [PATCH 22/36] Update TShockAPI/Commands.cs Co-authored-by: punchready <22683812+punchready@users.noreply.github.com> --- TShockAPI/Commands.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index a4669b04..d39b84e0 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -6462,7 +6462,7 @@ namespace TShockAPI if (target == user) user.SendSuccessMessage(GetString($"You buffed yourself with {TShock.Utils.GetBuffName(id)} ({TShock.Utils.GetBuffDescription(id)}) for {time} seconds.")); else - user.SendSuccessMessage(GetString($"You have buffed {user.Name} with {TShock.Utils.GetBuffName(id)} ({TShock.Utils.GetBuffDescription(id)}) for {time} seconds!")); + user.SendSuccessMessage(GetString($"You have buffed {target.Name} with {TShock.Utils.GetBuffName(id)} ({TShock.Utils.GetBuffDescription(id)}) for {time} seconds!")); if (!args.Silent && target != user) target.SendSuccessMessage(GetString($"{user.Name} has buffed you with {TShock.Utils.GetBuffName(id)} ({TShock.Utils.GetBuffDescription(id)}) for {time} seconds!")); } From b1a054472fcabd14182c9dd1ec7fa3871ab9eb39 Mon Sep 17 00:00:00 2001 From: Cardinal System Date: Tue, 15 Aug 2023 09:53:09 +0000 Subject: [PATCH 23/36] =?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 | 600 +++++++++++++++++++++++----------------------- 1 file changed, 296 insertions(+), 304 deletions(-) diff --git a/i18n/template.pot b/i18n/template.pot index a4fe57ab..9da0b2e0 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 -#, csharp-format -msgid "" -"Bouncer / SendTileRect processing a wall conversion update - [{0}] -> [{1}]" -msgstr "" - -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:129 -#, csharp-format -msgid "Bouncer / SendTileRect reimplemented from carbonara from {0}" -msgstr "" - -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:299 -msgid "Bouncer / SendTileRect rejected for banned tile" -msgstr "" - -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:578 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:446 #, csharp-format msgid "Bouncer / SendTileRect rejected from being disabled from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:293 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:518 +#, csharp-format +msgid "Bouncer / SendTileRect rejected from matches from {0}" +msgstr "" + +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:459 #, csharp-format msgid "" -"Bouncer / SendTileRect rejected from no permission for tile object from {0}" +"Bouncer / SendTileRect rejected from out of bounds / build permission from " +"{0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:565 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:478 #, csharp-format -msgid "Bouncer / SendTileRect rejected from non-vanilla tilemod from {0}" +msgid "Bouncer / SendTileRect rejected from out of range from {0}" msgstr "" -#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:571 +#: ../../TShockAPI/Handlers/SendTileRectHandler.cs:427 +#, csharp-format +msgid "Bouncer / SendTileRect rejected from size from {0}" +msgstr "" + +#: ../../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 24/36] 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 b20ac5b6..234117c0 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 6a159e8a..035df8ad 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 25/36] 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 186bee70..87024fb9 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 0b89681b..ca018510 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 26/36] 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 00000000..deb811a2 --- /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 035df8ad..3cb52f80 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 27/36] 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 070c6c88..ae3bb29e 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 @@ namespace TShockAPI public TextLog(string filename, bool clear) { FileName = filename; - _logWriter = new StreamWriter(filename, !clear); + ClearFile = clear; } public bool MayWriteType(TraceLevel type) @@ -247,6 +248,10 @@ namespace TShockAPI { if (!MayWriteType(level)) return; + if (_logWriter is null) + { + _logWriter = new StreamWriter(FileName, !ClearFile); + } var caller = "TShock"; @@ -278,7 +283,10 @@ namespace TShockAPI public void Dispose() { - _logWriter.Dispose(); + if (_logWriter != null) + { + _logWriter.Dispose(); + } } } } diff --git a/docs/changelog.md b/docs/changelog.md index 035df8ad..ebb335cc 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 28/36] 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 6ff7fd1a..58c14b1c 100644 --- a/TShockAPI/Bouncer.cs +++ b/TShockAPI/Bouncer.cs @@ -2854,7 +2854,7 @@ namespace TShockAPI { 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 27e037d6..b1974546 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 29/36] 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 054e31df..bcd72aa3 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -428,6 +428,8 @@ namespace TShockAPI Hooks.AccountHooks.AccountDelete += OnAccountDelete; Hooks.AccountHooks.AccountCreate += OnAccountCreate; + On.Terraria.RemoteClient.Reset += RemoteClient_Reset; + GetDataHandlers.InitGetDataHandler(); Commands.InitCommands(); @@ -496,6 +498,12 @@ namespace TShockAPI } } + 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 a3731709..9da8cd08 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 30/36] 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 8275735a..75b69d48 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 31/36] Update logo url in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5559b37..0369df53 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 32/36] 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 e038817e..65c5eba6 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 33/36] 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 33c503fd..774f9819 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 34/36] 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 774f9819..1d53f320 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 35/36] 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 9b73d66a..ac3bfff4 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 36/36] Fix changelog versioning --- docs/changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index b1974546..54b23a8a 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)