Completely rewrite STR handling

This commit is contained in:
punchready 2023-04-04 05:08:17 +02:00
parent 2c06e2bdf5
commit b714ab18a5
2 changed files with 843 additions and 2 deletions

View file

@ -0,0 +1,841 @@
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.ID;
using TShockAPI.Net;
namespace TShockAPI.Handlers
{
/// <summary>
/// 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.
/// </summary>
public sealed class SendTileRectHandlerRefactor : IPacketHandler<GetDataHandlers.SendTileRectEventArgs>
{
/// <summary>
/// Represents a tile rectangle sent through the packet.
/// </summary>
private sealed class TileRect
{
private readonly NetTile[,] _tiles;
public readonly int X;
public readonly int Y;
public readonly int Width;
public readonly int Height;
/// <summary>
/// Accesses the tiles contained in this rect.
/// </summary>
/// <param name="x">The X coordinate within the rect.</param>
/// <param name="y">The Y coordinate within the rect.</param>
/// <returns>The tile at the given position within the rect.</returns>
public NetTile this[int x, int y] => _tiles[x, y];
/// <summary>
/// Constructs a new tile rect based on the given information.
/// </summary>
public TileRect(NetTile[,] tiles, int x, int y, int width, int height)
{
_tiles = tiles;
X = x;
Y = y;
Width = width;
Height = height;
}
/// <summary>
/// Reads a tile rect from the given stream.
/// </summary>
/// <returns>The resulting tile rect.</returns>
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);
}
}
/// <summary>
/// Represents a common tile rect operation (Placement, State Change, Removal).
/// </summary>
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;
}
/// <summary>
/// Creates a new placement operation.
/// </summary>
/// <param name="width">The width of the placement.</param>
/// <param name="height">The height of the placement.</param>
/// <param name="tileType">The tile type of the placement.</param>
/// <param name="maxFrameX">The maximum allowed frameX of the placement, or <see cref="IGNORE_FRAME"/> if this operation does not change frameX.</param>
/// <param name="maxFrameY">The maximum allowed frameY of the placement, or <see cref="IGNORE_FRAME"/> if this operation does not change frameY.</param>
/// <returns>The resulting operation match.</returns>
public static TileRectMatch Placement(int width, int height, ushort tileType, short maxFrameX, short maxFrameY)
{
return new TileRectMatch(MatchType.Placement, width, height, tileType, maxFrameX, maxFrameY);
}
/// <summary>
/// Creates a new state change operation.
/// </summary>
/// <param name="width">The width of the state change.</param>
/// <param name="height">The height of the state change.</param>
/// <param name="tileType">The target tile type of the state change.</param>
/// <param name="maxFrameX">The maximum allowed frameX of the state change, or <see cref="IGNORE_FRAME"/> if this operation does not change frameX.</param>
/// <param name="maxFrameY">The maximum allowed frameY of the state change, or <see cref="IGNORE_FRAME"/> if this operation does not change frameY.</param>
/// <returns>The resulting operation match.</returns>
public static TileRectMatch StateChange(int width, int height, ushort tileType, short maxFrameX, short maxFrameY)
{
return new TileRectMatch(MatchType.StateChange, width, height, tileType, maxFrameX, maxFrameY);
}
/// <summary>
/// Creates a new removal operation.
/// </summary>
/// <param name="width">The width of the removal.</param>
/// <param name="height">The height of the removal.</param>
/// <param name="tileType">The target tile type of the removal.</param>
/// <returns>The resulting operation match.</returns>
public static TileRectMatch Removal(int width, int height, ushort tileType)
{
return new TileRectMatch(MatchType.Removal, width, height, tileType, 0, 0);
}
/// <summary>
/// Determines whether the given tile rectangle matches this operation, and if so, applies it to the world.
/// </summary>
/// <param name="player">The player the operation originates from.</param>
/// <param name="rect">The tile rectangle of the operation.</param>
/// <returns><see langword="true"/>, if the rect matches this operation and the changes have been applied, otherwise <see langword="false"/>.</returns>
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;
}
}
/// <summary>
/// Contains the complete list of valid tile rect operations the game currently performs.
/// </summary>
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),
};
/// <summary>
/// Handles a packet receive event.
/// </summary>
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;
}
/// <summary>
/// Checks whether the tile rect is at a valid position for the given player.
/// </summary>
/// <param name="player">The player the operation originates from.</param>
/// <param name="rect">The tile rectangle of the operation.</param>
/// <returns><see langword="true"/>, if the rect at a valid position, otherwise <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Checks whether the tile rect is at a valid distance to the given player.
/// </summary>
/// <param name="player">The player the operation originates from.</param>
/// <param name="rect">The tile rectangle of the operation.</param>
/// <returns><see langword="true"/>, if the rect at a valid distance, otherwise <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Checks whether the tile rect is a valid conversion spread (Clentaminator, Powders, etc.)
/// </summary>
/// <param name="player">The player the operation originates from.</param>
/// <param name="rect">The tile rectangle of the operation.</param>
/// <returns><see langword="true"/>, if the rect matches a conversion spread operation, otherwise <see langword="false"/>.</returns>
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<ushort, HashSet<ushort>> PlantToGrassMap = new Dictionary<ushort, HashSet<ushort>>
{
{ TileID.Plants, new HashSet<ushort>()
{
TileID.Grass, TileID.GolfGrass
} },
{ TileID.HallowedPlants, new HashSet<ushort>()
{
TileID.HallowedGrass, TileID.GolfGrassHallowed
} },
{ TileID.HallowedPlants2, new HashSet<ushort>()
{
TileID.HallowedGrass, TileID.GolfGrassHallowed
} },
{ TileID.JunglePlants2, new HashSet<ushort>()
{
TileID.JungleGrass
} },
{ TileID.AshPlants, new HashSet<ushort>()
{
TileID.AshGrass
} },
};
private static readonly Dictionary<ushort, HashSet<ushort>> GrassToStyleMap = new Dictionary<ushort, HashSet<ushort>>()
{
{ TileID.Plants, new HashSet<ushort>()
{
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<ushort>()
{
4, 6,
} },
{ TileID.HallowedPlants2, new HashSet<ushort>()
{
2, 3, 4, 6, 7,
} },
{ TileID.JunglePlants2, new HashSet<ushort>()
{
9, 10, 11, 12, 13, 14, 15, 16,
} },
{ TileID.AshPlants, new HashSet<ushort>()
{
6, 7, 8, 9, 10,
} },
};
/// <summary>
/// Checks whether the tile rect is a valid Flower Boots placement.
/// </summary>
/// <param name="player">The player the operation originates from.</param>
/// <param name="rect">The tile rectangle of the operation.</param>
/// <returns><see langword="true"/>, if the rect matches a Flower Boots placement, otherwise <see langword="false"/>.</returns>
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<ushort> 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<ushort, ushort> GrassToMowedMap = new Dictionary<ushort, ushort>
{
{ TileID.Grass, TileID.GolfGrass },
{ TileID.HallowedGrass, TileID.GolfGrassHallowed },
};
/// <summary>
/// Checks whether the tile rect is a valid grass mow.
/// </summary>
/// <param name="player">The player the operation originates from.</param>
/// <param name="rect">The tile rectangle of the operation.</param>
/// <returns><see langword="true"/>, if the rect matches a grass mowing operation, otherwise <see langword="false"/>.</returns>
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;
}
/// <summary>
/// Checks whether the tile rect is a valid christmas tree modification.
/// This also required significant reverse engineering effort.
/// </summary>
/// <param name="player">The player the operation originates from.</param>
/// <param name="rect">The tile rectangle of the operation.</param>
/// <returns><see langword="true"/>, if the rect matches a christmas tree operation, otherwise <see langword="false"/>.</returns>
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;
}
}
}