diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eda1a4c..2078fd4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,11 @@ This is the rolling changelog for TShock for Terraria. Use past tense when adding new entries; sign your name off when you add or change something. This should primarily be things like user changes, not necessarily codebase changes unless it's really relevant or large. ## Upcoming release -* Your change goes here! +* Fix all rope coils. (@Olink) +* Fixed a longstanding issue with SendTileSquare that could result in desyncs and visual errors. (@QuiCM) +* Fixed placement issues with Item Frames, Teleportation Pylons, etc. (@QuiCM) +* Doors are good now for real probably (@QuiCM, @Hakusaro, @Olink) +* Bump default max damage received cap to 42,000 to accommodate the Empress of Light's instant kill death amount. (@hakusaro, @moisterrific, @Irethia, @Ayrawei) ## TShock 4.4.0 (Pre-release 9) * Fixed pet licenses. (@Olink) diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs index 09c06f3c..395bfddd 100644 --- a/TShockAPI/Bouncer.cs +++ b/TShockAPI/Bouncer.cs @@ -36,15 +36,19 @@ namespace TShockAPI /// Bouncer is the TShock anti-hack and anti-cheat system. internal sealed class Bouncer { + internal Handlers.SendTileSquareHandler STSHandler { get; set; } + /// Constructor call initializes Bouncer and related functionality. /// A new Bouncer. internal Bouncer() { + STSHandler = new Handlers.SendTileSquareHandler(); + GetDataHandlers.SendTileSquare += STSHandler.OnReceiveSendTileSquare; + // Setup hooks GetDataHandlers.GetSection += OnGetSection; GetDataHandlers.PlayerUpdate += OnPlayerUpdate; GetDataHandlers.TileEdit += OnTileEdit; - GetDataHandlers.SendTileSquare += OnSendTileSquare; GetDataHandlers.ItemDrop += OnItemDrop; GetDataHandlers.NewProjectile += OnNewProjectile; GetDataHandlers.NPCStrike += OnNPCStrike; @@ -297,6 +301,19 @@ namespace TShockAPI { args.Player.LastKilledProjectile = 0; } + else if (CoilTileIds.Contains(editData)) + { + //projectile should be the same X coordinate as all tile places + if (!args.Player.RecentlyCreatedProjectiles.Any(p => GetDataHandlers.projectileCreatesTile.ContainsKey(Main.projectile[p.Index].type) && + Math.Abs((int)(Main.projectile[p.Index].position.X / 16f) - tileX) <= Math.Abs(Main.projectile[p.Index].velocity.X) && + GetDataHandlers.projectileCreatesTile[Main.projectile[p.Index].type] == editData)) + { + TShock.Log.ConsoleDebug("Bouncer / OnTileEdit rejected from (inconceivable rope coil) {0} {1} {2}", args.Player.Name, action, editData); + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + } else if (action == EditAction.PlaceTile || action == EditAction.PlaceWall) { if ((action == EditAction.PlaceTile && TShock.Config.PreventInvalidPlaceStyle) && @@ -518,213 +535,6 @@ namespace TShockAPI } } - /// Bouncer's SendTileSquare hook halts large scope world destruction. - /// The object that triggered the event. - /// The packet arguments that the event has. - internal void OnSendTileSquare(object sender, GetDataHandlers.SendTileSquareEventArgs args) - { - short size = args.Size; - int tileX = args.TileX; - int tileY = args.TileY; - - if (args.Player.HasPermission(Permissions.allowclientsideworldedit)) - { - TShock.Log.ConsoleDebug("Bouncer / SendTileSquare accepted clientside world edit from {0}", args.Player.Name); - args.Handled = false; - return; - } - - // From White: - // IIRC it's because 5 means a 5x5 square which is normal for a tile square, and anything bigger is a non-vanilla tile modification attempt - if (size > 5) - { - TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from non-vanilla tilemod from {0}", args.Player.Name); - args.Handled = true; - return; - } - - if (args.Player.IsBouncerThrottled()) - { - TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from throttle from {0}", args.Player.Name); - args.Player.SendTileSquare(tileX, tileY, size); - args.Handled = true; - return; - } - - if (args.Player.IsBeingDisabled()) - { - TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from being disabled from {0}", args.Player.Name); - args.Player.SendTileSquare(tileX, tileY, size); - args.Handled = true; - return; - } - - bool changed = false; - bool failed = false; - bool doorRelated = false; - try - { - var tiles = new NetTile[size, size]; - for (int x = 0; x < size; x++) - { - for (int y = 0; y < size; y++) - { - tiles[x, y] = new NetTile(args.Data); - } - } - - for (int x = 0; x < size; x++) - { - int realx = tileX + x; - if (realx < 0 || realx >= Main.maxTilesX) - continue; - - for (int y = 0; y < size; y++) - { - int realy = tileY + y; - if (realy < 0 || realy >= Main.maxTilesY) - continue; - - var tile = Main.tile[realx, realy]; - var newtile = tiles[x, y]; - if (!args.Player.HasBuildPermission(realx, realy) || - !args.Player.IsInRange(realx, realy)) - { - continue; - } - - if (newtile.Active && Terraria.ID.TileID.Sets.RoomNeeds.CountsAsDoor.Contains(newtile.Type)) - { - doorRelated = true; - } - - // Fixes the Flower Boots not creating flowers issue - if (size == 1 && args.Player.Accessories.Any(i => i.active && i.netID == ItemID.FlowerBoots)) - { - if (Main.tile[realx, realy + 1].type == TileID.Grass && (newtile.Type == TileID.Plants || newtile.Type == TileID.Plants2)) - { - args.Handled = false; - return; - } - - if (Main.tile[realx, realy + 1].type == TileID.HallowedGrass && (newtile.Type == TileID.HallowedPlants || newtile.Type == TileID.HallowedPlants2)) - { - args.Handled = false; - return; - } - - if (Main.tile[realx, realy + 1].type == TileID.JungleGrass && newtile.Type == TileID.JunglePlants2) - { - args.Handled = false; - return; - } - } - - // Junction Box - if (tile.type == TileID.WirePipe) - { - args.Handled = false; - return; - } - - // Orientable tiles - if (tile.type == newtile.Type && orientableTiles.Contains(tile.type)) - { - Main.tile[realx, realy].frameX = newtile.FrameX; - Main.tile[realx, realy].frameY = newtile.FrameY; - changed = true; - } - - // Landmine - if (tile.type == TileID.LandMine && !newtile.Active) - { - Main.tile[realx, realy].active(false); - changed = true; - } - - // Tile entities: sensors, item frames, training dummies - // here it handles all tile entities listed in `TileEntityID` - if ((newtile.Type == TileID.LogicSensor || - newtile.Type == TileID.ItemFrame || - newtile.Type == TileID.TargetDummy) && - !Main.tile[realx, realy].active()) - { - Main.tile[realx, realy].type = newtile.Type; - Main.tile[realx, realy].frameX = newtile.FrameX; - Main.tile[realx, realy].frameY = newtile.FrameY; - Main.tile[realx, realy].active(true); - changed = true; - } - - if (tile.active() && newtile.Active && tile.type != newtile.Type) - { - // Grass <-> Grass - if ((TileID.Sets.Conversion.Grass[tile.type] && TileID.Sets.Conversion.Grass[newtile.Type]) || - // Dirt <-> Dirt - ((tile.type == 0 || tile.type == 59) && - (newtile.Type == 0 || newtile.Type == 59)) || - // Ice <-> Ice - (TileID.Sets.Conversion.Ice[tile.type] && TileID.Sets.Conversion.Ice[newtile.Type]) || - // Stone <-> Stone - ((TileID.Sets.Conversion.Stone[tile.type] || Main.tileMoss[tile.type]) && - (TileID.Sets.Conversion.Stone[newtile.Type] || Main.tileMoss[newtile.Type])) || - // Sand <-> Sand - (TileID.Sets.Conversion.Sand[tile.type] && TileID.Sets.Conversion.Sand[newtile.Type]) || - // Sandstone <-> Sandstone - (TileID.Sets.Conversion.Sandstone[tile.type] && TileID.Sets.Conversion.Sandstone[newtile.Type]) || - // Hardened Sand <-> Hardened Sand - (TileID.Sets.Conversion.HardenedSand[tile.type] && TileID.Sets.Conversion.HardenedSand[newtile.Type])) - { - Main.tile[realx, realy].type = newtile.Type; - changed = true; - } - } - - // Stone wall <-> Stone wall - if (((tile.wall == 1 || tile.wall == 3 || tile.wall == 28 || tile.wall == 83) && - (newtile.Wall == 1 || newtile.Wall == 3 || newtile.Wall == 28 || newtile.Wall == 83)) || - // Leaf wall <-> Leaf wall - (((tile.wall >= 63 && tile.wall <= 70) || tile.wall == 81) && - ((newtile.Wall >= 63 && newtile.Wall <= 70) || newtile.Wall == 81))) - { - Main.tile[realx, realy].wall = newtile.Wall; - changed = true; - } - - if ((tile.type == TileID.TrapdoorClosed && (newtile.Type == TileID.TrapdoorOpen || !newtile.Active)) || - (tile.type == TileID.TrapdoorOpen && (newtile.Type == TileID.TrapdoorClosed || !newtile.Active)) || - (!tile.active() && newtile.Active && (newtile.Type == TileID.TrapdoorOpen || newtile.Type == TileID.TrapdoorClosed))) - { - Main.tile[realx, realy].type = newtile.Type; - Main.tile[realx, realy].frameX = newtile.FrameX; - Main.tile[realx, realy].frameY = newtile.FrameY; - Main.tile[realx, realy].active(newtile.Active); - changed = true; - } - } - } - - if (changed) - { - TSPlayer.All.SendTileSquare(tileX, tileY, size + 1); - WorldGen.RangeFrame(tileX, tileY, tileX + size, tileY + size); - } - else - { - if (!doorRelated) - args.Player.SendTileSquare(tileX, tileY, size); - } - } - catch - { - args.Player.SendTileSquare(tileX, tileY, size); - failed = true; - } - - TShock.Log.ConsoleDebug("Bouncer / SendTileSquare from {0} {1} {2}", args.Player.Name, changed, failed); - args.Handled = true; - } - /// Registered when items fall to the ground to prevent cheating. /// The object that triggered the event. /// The packet arguments that the event has. @@ -2044,7 +1854,7 @@ namespace TShockAPI short id = args.PlayerId; PlayerDeathReason playerDeathReason = args.PlayerDeathReason; - if (damage > 20000) //Abnormal values have the potential to cause infinite loops in the server. + if (damage > 42000) //Abnormal values have the potential to cause infinite loops in the server. { TShock.Log.ConsoleDebug("Bouncer / OnKillMe rejected high damage from {0} {1}", args.Player.Name, damage); args.Player.Kick("Failed to shade polygon normals.", true, true); diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs index 21870ce4..304de4e2 100644 --- a/TShockAPI/GetDataHandlers.cs +++ b/TShockAPI/GetDataHandlers.cs @@ -2357,14 +2357,14 @@ namespace TShockAPI { var player = args.Player; var size = args.Data.ReadInt16(); - var changeType = TileChangeType.None; + bool hasChangeType = ((size & 0x7FFF) & 0x8000) != 0; if (hasChangeType) { changeType = (TileChangeType)args.Data.ReadInt8(); } - + var tileX = args.Data.ReadInt16(); var tileY = args.Data.ReadInt16(); var data = args.Data; @@ -3813,7 +3813,20 @@ namespace TShockAPI { ProjectileID.EbonsandBallGun, TileID.Ebonsand }, { ProjectileID.PearlSandBallGun, TileID.Pearlsand }, { ProjectileID.CrimsandBallGun, TileID.Crimsand }, - { ProjectileID.MysticSnakeCoil, TileID.MysticSnakeRope } + { ProjectileID.MysticSnakeCoil, TileID.MysticSnakeRope }, + { ProjectileID.RopeCoil, TileID.Rope }, + { ProjectileID.SilkRopeCoil, TileID.SilkRope }, + { ProjectileID.VineRopeCoil, TileID.VineRope }, + { ProjectileID.WebRopeCoil, TileID.WebRope } + }; + + internal static List CoilTileIds = new List() + { + TileID.MysticSnakeRope, + TileID.Rope, + TileID.SilkRope, + TileID.VineRope, + TileID.WebRope }; internal static Dictionary projectileCreatesLiquid = new Dictionary diff --git a/TShockAPI/Handlers/SendTileSquareHandler.cs b/TShockAPI/Handlers/SendTileSquareHandler.cs new file mode 100644 index 00000000..48462a03 --- /dev/null +++ b/TShockAPI/Handlers/SendTileSquareHandler.cs @@ -0,0 +1,504 @@ +using OTAPI.Tile; +using System; +using System.Collections.Generic; +using System.Linq; +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 Square packets + /// + public class SendTileSquareHandler + { + /// + /// Maps grass-type blocks to flowers that can be grown on them with flower boots + /// + Dictionary> _grassToPlantMap = new Dictionary> + { + { TileID.Grass, new List { TileID.Plants, TileID.Plants2 } }, + { TileID.HallowedGrass, new List { TileID.HallowedPlants, TileID.HallowedPlants2 } }, + { TileID.JungleGrass, new List { TileID.JunglePlants, TileID.JunglePlants2 } } + }; + + /// + /// Item IDs that can spawn flowers while you walk + /// + List _flowerBootItems = new List + { + 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 + /// + 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 SendTileSquare packet is received + /// + /// + /// + public void OnReceiveSendTileSquare(object sender, GetDataHandlers.SendTileSquareEventArgs args) + { + // By default, we'll handle everything + args.Handled = true; + + if (ShouldSkipProcessing(args)) + { + return; + } + + bool[,] processed = new bool[args.Size, args.Size]; + NetTile[,] tiles = ReadNetTilesFromStream(args.Data, args.Size); + + Debug.VisualiseTileSetDiff(args.TileX, args.TileY, args.Size, args.Size, tiles); + + IterateTileSquare(tiles, processed, args); + + // Uncommenting this function will send the same tile square 10 blocks above you for visualisation. This will modify your world and overwrite existing blocks. + // Use in test worlds only. + //Debug.DisplayTileSetInGame(tileX, tileY - 10, size, size, 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) + { + args.Player.SendTileSquare(args.TileX, args.TileY, args.Size); + TShock.Log.ConsoleDebug("Bouncer / SendTileSquare reimplemented from carbonara from {0}", args.Player.Name); + } + } + + /// + /// Iterates over each tile in the tile square and performs processing on individual tiles or multi-tile Tile Objects + /// + /// + /// + /// + internal void IterateTileSquare(NetTile[,] tiles, bool[,] processed, GetDataHandlers.SendTileSquareEventArgs args) + { + short size = args.Size; + int tileX = args.TileX; + int tileY = args.TileY; + + for (int x = 0; x < size; x++) + { + for (int y = 0; y < size; 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) + { + data = TileObjectData._data[newTile.Type]; + NetTile[,] newTiles; + int width = data.Width; + int height = data.Height; + int offset = 0; + + if (newTile.Type == TileID.TrapdoorClosed) + { + // Trapdoors can modify a 2x3 space. When it closes it will have leftover tiles either on top or bottom. + // If we don't update these tiles, the trapdoor gets confused and disappears. + // So we capture all 6 possible tiles and offset ourselves 1 tile above the closed trapdoor to capture the entire 2x3 area + width = 2; + height = 3; + offset = -1; + } + + newTiles = new NetTile[width, height]; + + for (int i = 0; i < width; i++) + { + for (int j = 0; j < height; j++) + { + newTiles[i, j] = tiles[x + i, y + j + offset]; + processed[x + i, y + j] = true; + } + } + ProcessTileObject(newTile.Type, realX, realY + offset, width, height, newTiles, args); + continue; + } + + // If the new tile does not have an associated tile object, process it as an individual tile + ProcessSingleTile(realX, realY, newTile, size, args); + processed[x, y] = true; + } + } + } + + /// + /// Processes a tile object consisting of multiple tiles from the tile square 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 + /// SendTileSquareEventArgs containing event information + internal void ProcessTileObject(int tileType, int realX, int realY, int width, int height, NetTile[,] newTiles, GetDataHandlers.SendTileSquareEventArgs 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("Bouncer / SendTileSquare rejected from no permission for tile object from {0}", args.Player.Name); + return; + } + + // 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 square 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 size of the tile square being received + /// SendTileSquareEventArgs containing event information + internal void ProcessSingleTile(int realX, int realY, NetTile newTile, int squareSize, GetDataHandlers.SendTileSquareEventArgs args) + { + // Some boots allow growing flowers on grass. This process sends a 1x1 tile square to grow the flowers + // The square size must be 1 and the player must have an accessory that allows growing flowers in order for this square to be valid + if (squareSize == 1 && args.Player.Accessories.Any(a => a != null && _flowerBootItems.Contains(a.type))) + { + ProcessFlowerBoots(realX, realY, newTile, args); + return; + } + + ITile tile = Main.tile[realX, realY]; + + if (tile.type == TileID.LandMine && !newTile.Active) + { + UpdateServerTileState(tile, newTile); + } + + if (tile.type == TileID.WirePipe) + { + UpdateServerTileState(tile, newTile); + } + + ProcessConversionSpreads(Main.tile[realX, realY], newTile); + + // All other single tile updates should not be processed. + } + + /// + /// Applies changes to a tile if a tile square for flower-growing boots is valid + /// + /// The tile x position of the tile square packet - this is where the flowers are intending to grow + /// The tile y position of the tile square packet - this is where the flowers are intending to grow + /// The NetTile containing information about the flowers that are being grown + /// SendTileSquareEventArgs containing event information + internal void ProcessFlowerBoots(int realX, int realY, NetTile newTile, GetDataHandlers.SendTileSquareEventArgs args) + { + // We need to get the tile below the tile square to determine what grass types are allowed + if (!WorldGen.InWorld(realX, realY + 1)) + { + // If the tile below the tile square isn't valid, we return here and don't update the server tile state + return; + } + + ITile tile = Main.tile[realX, realY + 1]; + if (!_grassToPlantMap.TryGetValue(tile.type, out List plantTiles) && !plantTiles.Contains(newTile.Type)) + { + // If the tile below the tile square isn't a valid plant tile (eg grass) then we don't update the server tile state + return; + } + + UpdateServerTileState(Main.tile[realX, realY], newTile); + } + + /// + /// 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) + { + // Update if the existing tile or wall is convertible and the new tile or wall is a valid conversion + if ( + ((TileID.Sets.Conversion.Stone[tile.type] || Main.tileMoss[tile.type]) && (TileID.Sets.Conversion.Stone[newTile.Type] || Main.tileMoss[newTile.Type])) || + ((tile.type == 0 || tile.type == 59) && (newTile.Type == 0 || newTile.Type == 59)) || + TileID.Sets.Conversion.Grass[tile.type] && TileID.Sets.Conversion.Grass[newTile.Type] || + TileID.Sets.Conversion.Ice[tile.type] && TileID.Sets.Conversion.Ice[newTile.Type] || + TileID.Sets.Conversion.Sand[tile.type] && TileID.Sets.Conversion.Sand[newTile.Type] || + TileID.Sets.Conversion.Sandstone[tile.type] && TileID.Sets.Conversion.Sandstone[newTile.Type] || + TileID.Sets.Conversion.HardenedSand[tile.type] && TileID.Sets.Conversion.HardenedSand[newTile.Type] || + TileID.Sets.Conversion.Thorn[tile.type] && TileID.Sets.Conversion.Thorn[newTile.Type] || + TileID.Sets.Conversion.Moss[tile.type] && TileID.Sets.Conversion.Moss[newTile.Type] || + TileID.Sets.Conversion.MossBrick[tile.type] && TileID.Sets.Conversion.MossBrick[newTile.Type] || + WallID.Sets.Conversion.Stone[tile.wall] && WallID.Sets.Conversion.Stone[newTile.Wall] || + WallID.Sets.Conversion.Grass[tile.wall] && WallID.Sets.Conversion.Grass[newTile.Wall] || + WallID.Sets.Conversion.Sandstone[tile.wall] && WallID.Sets.Conversion.Sandstone[newTile.Wall] || + WallID.Sets.Conversion.HardenedSand[tile.wall] && WallID.Sets.Conversion.HardenedSand[newTile.Wall] || + WallID.Sets.Conversion.PureSand[tile.wall] && WallID.Sets.Conversion.PureSand[newTile.Wall] || + WallID.Sets.Conversion.NewWall1[tile.wall] && WallID.Sets.Conversion.NewWall1[newTile.Wall] || + WallID.Sets.Conversion.NewWall2[tile.wall] && WallID.Sets.Conversion.NewWall2[newTile.Wall] || + WallID.Sets.Conversion.NewWall3[tile.wall] && WallID.Sets.Conversion.NewWall3[newTile.Wall] || + WallID.Sets.Conversion.NewWall4[tile.wall] && WallID.Sets.Conversion.NewWall4[newTile.Wall] + ) + { + TShock.Log.ConsoleDebug("Bouncer / SendTileSquare processing a conversion update - [{0}|{1}] -> [{2}|{3}]", tile.type, tile.wall, newTile.Type, newTile.Wall); + UpdateServerTileState(tile, newTile); + } + } + + /// + /// Updates a single tile's world state with a change from the tile square packet + /// + /// The tile to update + /// The NetTile containing the change + public static void UpdateServerTileState(ITile tile, NetTile newTile) + { + tile.active(newTile.Active); + tile.type = newTile.Type; + + if (newTile.FrameImportant) + { + tile.frameX = newTile.FrameX; + tile.frameY = newTile.FrameY; + } + + if (newTile.HasWall) + { + tile.wall = newTile.Wall; + } + + if (newTile.HasLiquid) + { + tile.liquid = newTile.Liquid; + tile.liquidType(newTile.LiquidType); + } + + tile.wire(newTile.Wire); + tile.wire2(newTile.Wire2); + tile.wire3(newTile.Wire3); + tile.wire4(newTile.Wire4); + + tile.halfBrick(newTile.IsHalf); + + if (newTile.HasColor) + { + tile.color(newTile.TileColor); + } + + if (newTile.HasWallColor) + { + tile.wallColor(newTile.WallColor); + } + + byte slope = 0; + if (newTile.Slope) + { + slope += 1; + } + if (newTile.Slope2) + { + slope += 2; + } + if (newTile.Slope3) + { + slope += 4; + } + + tile.slope(slope); + + TShock.Log.ConsoleDebug("Bouncer / SendTileSquare updated a tile from type {0} to {1}", tile.type, newTile.Type); + } + + /// + /// 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]); + } + } + } + + /// + /// Reads a set of NetTiles from a memory stream + /// + /// + /// + /// + static NetTile[,] ReadNetTilesFromStream(System.IO.MemoryStream stream, int size) + { + NetTile[,] tiles = new NetTile[size, size]; + for (int x = 0; x < size; x++) + { + for (int y = 0; y < size; y++) + { + tiles[x, y] = new NetTile(stream); + } + } + + return tiles; + } + + /// + /// Determines whether or not the tile square should be immediately accepted or rejected + /// + /// + /// + static bool ShouldSkipProcessing(GetDataHandlers.SendTileSquareEventArgs args) + { + if (args.Player.HasPermission(Permissions.allowclientsideworldedit)) + { + TShock.Log.ConsoleDebug("Bouncer / SendTileSquare accepted clientside world edit from {0}", args.Player.Name); + args.Handled = false; + return true; + } + + // 5x5 is the largest vanilla-sized tile square. Anything larger than this should not be seen in the vanilla game and should be rejected + if (args.Size > 5) + { + TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from non-vanilla tilemod from {0}", args.Player.Name); + return true; + } + + if (args.Player.IsBouncerThrottled()) + { + TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from throttle from {0}", args.Player.Name); + args.Player.SendTileSquare(args.TileX, args.TileY, args.Size); + return true; + } + + if (args.Player.IsBeingDisabled()) + { + TShock.Log.ConsoleDebug("Bouncer / SendTileSquare rejected from being disabled from {0}", args.Player.Name); + args.Player.SendTileSquare(args.TileX, args.TileY, args.Size); + return true; + } + + return false; + } + + class Debug + { + /// + /// Displays the difference in IDs between existing tiles and a set of NetTiles to the console + /// + /// X position at the top left of the square + /// Y position at the top left of the square + /// 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 (TShock.Config.DebugLogs) + { + 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"); + } + } + } + + /// + /// Sends a tile square at the given (tileX, tileY) coordinate, using the given set of NetTiles information to update the tile square + /// + /// X position at the top left of the square + /// Y position at the top left of the square + /// Width of the NetTile set + /// Height of the NetTile set + /// New tiles to place in the square + /// Player to send the debug display to + public static void DisplayTileSetInGame(int tileX, int tileY, int width, int height, NetTile[,] newTiles, TSPlayer player) + { + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + UpdateServerTileState(Main.tile[tileX + x, tileY + y], newTiles[x, y]); + } + //Add a line of dirt blocks at the bottom for safety + UpdateServerTileState(Main.tile[tileX + x, tileY + height], new NetTile { Active = true, Type = 0 }); + } + + player.SendTileSquare(tileX, tileY, Math.Max(width, height) + 1); + } + } + } +} diff --git a/TShockAPI/Net/NetTile.cs b/TShockAPI/Net/NetTile.cs index 62896c10..62b362f9 100644 --- a/TShockAPI/Net/NetTile.cs +++ b/TShockAPI/Net/NetTile.cs @@ -36,6 +36,7 @@ namespace TShockAPI.Net public bool Wire { get; set; } public bool Wire2 { get; set; } public bool Wire3 { get; set; } + public bool Wire4 { get; set; } public byte HalfBrick { get; set; } public byte Actuator { get; set; } public bool Inactive { get; set; } @@ -83,6 +84,7 @@ namespace TShockAPI.Net Wire = false; Wire2 = false; Wire3 = false; + Wire4 = false; HalfBrick = 0; Actuator = 0; Inactive = false; @@ -151,8 +153,10 @@ namespace TShockAPI.Net if (Slope3) bits[6] = true; + if (Wire4) + bits[7] = true; - stream.WriteInt8((byte)bits); + stream.WriteByte(bits); if (HasColor) { @@ -175,7 +179,7 @@ namespace TShockAPI.Net } if (HasWall) - stream.WriteInt16((short)Wall);; + stream.WriteInt16((short)Wall); if (HasLiquid) { @@ -194,6 +198,7 @@ namespace TShockAPI.Net Slope = flags2[4]; Slope2 = flags2[5]; Slope3 = flags2[6]; + Wire4 = flags2[7]; if (flags2[2]) { diff --git a/TShockAPI/PlayerData.cs b/TShockAPI/PlayerData.cs index 562dea80..d6540e34 100644 --- a/TShockAPI/PlayerData.cs +++ b/TShockAPI/PlayerData.cs @@ -496,7 +496,7 @@ namespace TShockAPI } var response = NetCreativeUnlocksModule.SerializeItemSacrifice(i, amount); - NetManager.Instance.SendToClient(response, player.TPlayer.whoAmI); + NetManager.Instance.SendToClient(response, player.Index); } } } diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index 1a204594..4a85f590 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -620,7 +620,8 @@ namespace TShockAPI /// Determines if the player can build on a given point. /// The x coordinate they want to build at. - /// The y coordinate they want to paint at. + /// The y coordinate they want to build at. + /// Whether or not the player should be warned if their build attempt fails /// True if the player can build at the given point from build, spawn, and region protection. public bool HasBuildPermission(int x, int y, bool shouldWarnPlayer = true) { @@ -679,6 +680,32 @@ namespace TShockAPI return false; } + /// + /// Determines if the player can build a multi-block tile object on a given point. + /// Tile objects include things like Doors, Trap Doors, Item Frames, Beds, and Dressers. + /// + /// The x coordinate they want to build at. + /// The y coordinate they want to build at. + /// The width of the tile object + /// The height of the tile object + /// Whether or not the player should be warned if their build attempt fails + /// True if the player can build at the given point from build, spawn, and region protection. + public bool HasBuildPermissionForTileObject(int x, int y, int width, int height, bool shouldWarnPlayer = true) + { + for (int realx = x; realx < x + width; realx++) + { + for (int realy = y; realy < y + height; realy++) + { + if (!HasBuildPermission(realx, realy, shouldWarnPlayer)) + { + return false; + } + } + } + + return true; + } + /// Determines if the player can paint on a given point. Checks general build permissions, then paint. /// The x coordinate they want to paint at. /// The y coordinate they want to paint at. @@ -1241,50 +1268,14 @@ namespace TShockAPI /// The x coordinate to send. /// The y coordinate to send. /// The size square set of tiles to send. - /// Status if the tile square was sent successfully (i.e. no exceptions). + /// true if the tile square was sent successfully, else false public virtual bool SendTileSquare(int x, int y, int size = 10) { try { - int num = (size - 1) / 2; - int m_x = 0; - int m_y = 0; - - if (x - num < 0) - { - m_x = 0; - } - else - { - m_x = x - num; - } - - if (y - num < 0) - { - m_y = 0; - } - else - { - m_y = y - num; - } - - if (m_x + size > Main.maxTilesX) - { - m_x = Main.maxTilesX - size; - } - - if (m_y + size > Main.maxTilesY) - { - m_y = Main.maxTilesY - size; - } - - SendData(PacketTypes.TileSendSquare, "", size, m_x, m_y); + SendData(PacketTypes.TileSendSquare, "", size, x, y); return true; } - catch (IndexOutOfRangeException) - { - // This is expected if square exceeds array. - } catch (Exception ex) { TShock.Log.Error(ex.ToString()); diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index a0bc4e64..6d84817c 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -1639,7 +1639,8 @@ namespace TShockAPI if (e.number >= 0 && e.number < Main.projectile.Length) { var projectile = Main.projectile[e.number]; - if (projectile.active && projectile.owner >= 0 && GetDataHandlers.projectileCreatesLiquid.ContainsKey(projectile.type)) + if (projectile.active && projectile.owner >= 0 && + (GetDataHandlers.projectileCreatesLiquid.ContainsKey(projectile.type) || GetDataHandlers.projectileCreatesTile.ContainsKey(projectile.type))) { var player = Players[projectile.owner]; if (player != null) diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index f6a2d81c..ea6f4f48 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -88,6 +88,7 @@ + @@ -211,7 +212,7 @@ - +