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