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;
+ }
+ }
+}