diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 67a3ae87..8173267b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,68 +5,62 @@ on: [push, pull_request]
jobs:
build:
runs-on: windows-latest
+ strategy:
+ matrix:
+ mode: ["Debug", "Release"]
steps:
- - uses: actions/checkout@v1
+ - name: Git checkout
+ uses: actions/checkout@v1
with:
submodules: recursive
- - name: Install nuget
- run: choco install nuget.commandline
- - name: OTAPI Debug
- shell: cmd
+ - name: Install NuGet client
+ uses: nuget/setup-nuget@v1
+ - name: Restore NuGet packages
run: |
nuget restore .\TerrariaServerAPI\TShock.4.OTAPI.sln
- "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TShock.4.OTAPI.sln /p:Configuration=Debug
- cd .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Debug
- TShock.Modifications.Bootstrapper.exe
- - name: OTAPI Release
+ nuget restore TShock.sln
+ - name: Build OTAPI
shell: cmd
run: |
- nuget restore .\TerrariaServerAPI\TShock.4.OTAPI.sln
- "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TShock.4.OTAPI.sln /p:Configuration=Release
- cd .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Release
+ "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TShock.4.OTAPI.sln /p:Configuration=${{ matrix.mode }}
+ cd .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\${{ matrix.mode }}
TShock.Modifications.Bootstrapper.exe
- - name: TerrariaServerAPI Debug
+ - name: Build TerrariaServerAPI
shell: cmd
run: |
cd .\TerrariaServerAPI
- "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TerrariaServerAPI.csproj /p:Configuration=Debug
- - name: TShock Debug
+ "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TerrariaServerAPI.csproj /p:Configuration=${{ matrix.mode }}
+ - name: Build TShock
shell: cmd
run: |
- nuget restore TShock.sln
- "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TShockAPI\TShockAPI.csproj /p:Configuration=Debug
- - name: TerrariaServerAPI Release
- shell: cmd
- run: |
- cd .\TerrariaServerAPI
- "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TerrariaServerAPI.csproj /p:Configuration=Release
- - name: TShock Release
- shell: cmd
- run: |
- nuget restore TShock.sln
- "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TShockAPI\TShockAPI.csproj /p:Configuration=Release
+ "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TShockAPI\TShockAPI.csproj /p:Configuration=${{ matrix.mode }}
- name: Normalize release packaging
shell: cmd
run: |
- xcopy /Y prebuilts\*.* TShockAPI\bin\Release
- xcopy /Y prebuilts\*.* TShockAPI\bin\Debug
- mkdir TShockAPI\bin\Debug\ServerPlugins
- mkdir TShockAPI\bin\Release\ServerPlugins
- move TShockAPI\bin\Release\TShockAPI.dll TShockAPI\bin\Release\ServerPlugins
- move TShockAPI\bin\Debug\TShockAPI.dll TShockAPI\bin\Debug\ServerPlugins
- - uses: actions/upload-artifact@master
- with:
- name: Experimental TShock (not debug)
- path: TShockAPI\bin\Release
- - uses: actions/upload-artifact@master
+ xcopy /Y prebuilts\*.* TShockAPI\bin\${{ matrix.mode }}
+ mkdir TShockAPI\bin\${{ matrix.mode }}\ServerPlugins
+ move TShockAPI\bin\${{ matrix.mode }}\TShockAPI.dll TShockAPI\bin\${{ matrix.mode }}\ServerPlugins
+ - name: Upload TShock (Debug)
+ if: contains(matrix.mode, 'Debug')
+ uses: actions/upload-artifact@master
with:
name: Experimental TShock (debug)
path: TShockAPI\bin\Debug
- - uses: actions/upload-artifact@master
+ - name: Upload OTAPI Bootstrapper (Debug)
+ if: contains(matrix.mode, 'Debug')
+ uses: actions/upload-artifact@master
with:
name: Experimental (debug) OTAPI Bootstrapper
path: .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Debug\TShock.Modifications.Bootstrapper.exe
- - uses: actions/upload-artifact@master
+ - name: Upload TShock (Not Debug)
+ if: contains(matrix.mode, 'Release')
+ uses: actions/upload-artifact@master
+ with:
+ name: Experimental TShock (not debug)
+ path: TShockAPI\bin\Release
+ - name: Upload OTAPI Bootstrapper (Not Debug)
+ if: contains(matrix.mode, 'Release')
+ uses: actions/upload-artifact@master
with:
name: Experimental (not debug) OTAPI Bootstrapper
path: .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Release\TShock.Modifications.Bootstrapper.exe
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a69b28a0..2078fd4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,14 @@
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
+## Upcoming release
+* 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)
* Added initial support for Journey mode in SSC worlds. (@Olink)
* Made TShock database MySQL 8 compatible by escaping column names in our IQueryBuilder code. (Name `Groups` is a reserved element in this version, which is used in our `Region` table.) (@Patrikkk)
@@ -28,6 +35,8 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin
* The default group that gets this permission is `Guest` for the time being.
* To add this command to your guest group, give them `tshock.synclocalarea`, with `/group addperm guest tshock.synclocalarea`.
* This command may be removed at any time in the future (and will likely be removed when send tile square handling is fixed).
+* Add FishOutNPC event handler, which is called whenever a player fishes out an NPC using a fishing rod. Added antihack to Bouncer, to prevent unathorized and invalid mob spawning, by checking player action, NPC IDs and range. (@Patrikkk, @moisterrific)
+* Fixed smart door automatic door desync and deletion issue. (@hakusaro)
## TShock 4.4.0 (Pre-release 8)
* Update for OTAPI 2.0.0.36 and Terraria 1.4.0.4. (@hakusaro, @Patrikkk, @DeathCradle)
diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs
index edf05800..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;
@@ -67,6 +71,7 @@ namespace TShockAPI
GetDataHandlers.MassWireOperation += OnMassWireOperation;
GetDataHandlers.PlayerDamage += OnPlayerDamage;
GetDataHandlers.KillMe += OnKillMe;
+ GetDataHandlers.FishOutNPC += OnFishOutNPC;
GetDataHandlers.FoodPlatterTryPlacing += OnFoodPlatterTryPlacing;
}
@@ -296,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) &&
@@ -517,206 +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;
- 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;
- }
-
- // 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
- {
- 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.
@@ -2036,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);
@@ -2065,6 +1883,34 @@ namespace TShockAPI
}
}
+ ///
+ /// Called when the player fishes out an NPC.
+ ///
+ ///
+ ///
+ internal void OnFishOutNPC(object sender, GetDataHandlers.FishOutNPCEventArgs args)
+ {
+ var projectile = args.Player.RecentlyCreatedProjectiles.FirstOrDefault(p => Main.projectile[p.Index] != null && Main.projectile[p.Index].Name == "Bobber");
+
+ if (!FishingRodItemIDs.Contains(args.Player.SelectedItem.type) || Main.projectile[projectile.Index] == null || !FishableNpcIDs.Contains(args.NpcID))
+ {
+ TShock.Log.ConsoleDebug("Bouncer / OnFishOutNPC rejected invalid NPC spawning from {0}", args.Player.Name);
+ args.Handled = true;
+ return;
+ }
+ if (args.NpcID == NPCID.DukeFishron && !args.Player.HasPermission(Permissions.summonboss))
+ {
+ TShock.Log.ConsoleDebug("Bouncer / OnFishOutNPC rejected summon boss permissions from {0}", args.Player.Name);
+ args.Handled = true;
+ return;
+ }
+ if (args.Player.IsInRange(args.TileX, args.TileY, 55))
+ {
+ TShock.Log.ConsoleDebug("Bouncer / OnFishOutNPC rejected range checks from {0}", args.Player.Name);
+ args.Handled = true;
+ }
+ }
+
///
/// Called when a player is trying to place an item into a food plate.
///
diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs
index a81ff597..304de4e2 100644
--- a/TShockAPI/GetDataHandlers.cs
+++ b/TShockAPI/GetDataHandlers.cs
@@ -151,6 +151,7 @@ namespace TShockAPI
{ PacketTypes.CrystalInvasionStart, HandleOldOnesArmy },
{ PacketTypes.PlayerHurtV2, HandlePlayerDamageV2 },
{ PacketTypes.PlayerDeathV2, HandlePlayerKillMeV2 },
+ { PacketTypes.FishOutNPC, HandleFishOutNPC },
{ PacketTypes.FoodPlatterTryPlacing, HandleFoodPlatterTryPlacing },
{ PacketTypes.SyncRevengeMarker, HandleSyncRevengeMarker }
};
@@ -1865,6 +1866,45 @@ namespace TShockAPI
return args.Handled;
}
+ ///
+ /// For use in a FishOutNPC event.
+ ///
+ public class FishOutNPCEventArgs : GetDataHandledEventArgs
+ {
+ ///
+ /// The X world position of the spawning NPC.
+ ///
+ public ushort TileX { get; set; }
+ ///
+ /// The Y world position of the spawning NPC.
+ ///
+ public ushort TileY { get; set; }
+ ///
+ /// The NPC type that is being spawned.
+ ///
+ public short NpcID { get; set; }
+ }
+ ///
+ /// Called when a player fishes out an NPC.
+ ///
+ public static HandlerList FishOutNPC = new HandlerList();
+ private static bool OnFishOutNPC(TSPlayer player, MemoryStream data, ushort tileX, ushort tileY, short npcID)
+ {
+ if (FishOutNPC == null)
+ return false;
+
+ var args = new FishOutNPCEventArgs
+ {
+ Player = player,
+ Data = data,
+ TileX = tileX,
+ TileY = tileY,
+ NpcID = npcID
+ };
+ FishOutNPC.Invoke(null, args);
+ return args.Handled;
+ }
+
public class FoodPlatterTryPlacingEventArgs : GetDataHandledEventArgs
{
///
@@ -2317,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;
@@ -3629,6 +3669,19 @@ namespace TShockAPI
return false;
}
+
+ private static bool HandleFishOutNPC(GetDataHandlerArgs args)
+ {
+ ushort tileX = args.Data.ReadUInt16();
+ ushort tileY = args.Data.ReadUInt16();
+ short npcType = args.Data.ReadInt16();
+
+ if (OnFishOutNPC(args.Player, args.Data, tileX, tileY, npcType))
+ return true;
+
+ return false;
+ }
+
private static bool HandleFoodPlatterTryPlacing(GetDataHandlerArgs args)
{
short tileX = args.Data.ReadInt16();
@@ -3717,6 +3770,39 @@ namespace TShockAPI
TileID.Womannequin,
};
+ ///
+ /// List of Fishing rod item IDs.
+ ///
+ internal static readonly List FishingRodItemIDs = new List()
+ {
+ ItemID.WoodFishingPole,
+ ItemID.ReinforcedFishingPole,
+ ItemID.FiberglassFishingPole,
+ ItemID.FisherofSouls,
+ ItemID.GoldenFishingRod,
+ ItemID.MechanicsRod,
+ ItemID.SittingDucksFishingRod,
+ ItemID.Fleshcatcher,
+ ItemID.HotlineFishingHook,
+ ItemID.BloodFishingRod,
+ ItemID.ScarabFishingRod
+ };
+
+ ///
+ /// List of NPC IDs that can be fished out by the player.
+ ///
+ internal static readonly List FishableNpcIDs = new List()
+ {
+ NPCID.EyeballFlyingFish,
+ NPCID.ZombieMerman,
+ NPCID.GoblinShark,
+ NPCID.BloodEelHead,
+ NPCID.BloodEelBody,
+ NPCID.BloodEelTail,
+ NPCID.BloodNautilus,
+ NPCID.DukeFishron
+ };
+
///
/// These projectiles create tiles on death.
///
@@ -3727,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 @@
-
+