From 8784c5453cbd876b23794047148eb9d5c6a7915e Mon Sep 17 00:00:00 2001
From: Nightklp <120942242+NightKLP@users.noreply.github.com>
Date: Fri, 13 Mar 2026 23:31:37 +0800
Subject: [PATCH 1/8] Update Bouncer.cs
---
TShockAPI/Bouncer.cs | 43 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs
index 909a84d68..16e5ae96e 100644
--- a/TShockAPI/Bouncer.cs
+++ b/TShockAPI/Bouncer.cs
@@ -130,6 +130,7 @@ internal Bouncer()
GetDataHandlers.PlaceObject += OnPlaceObject;
GetDataHandlers.PlaceTileEntity += OnPlaceTileEntity;
GetDataHandlers.PlaceItemFrame += OnPlaceItemFrame;
+ GetDataHandlers.WeaponsRackTryPlacing += OnWeaponsRackTryPlacing;
GetDataHandlers.PortalTeleport += OnPlayerPortalTeleport;
GetDataHandlers.GemLockToggle += OnGemLockToggle;
GetDataHandlers.MassWireOperation += OnMassWireOperation;
@@ -2696,6 +2697,48 @@ internal void OnPlaceItemFrame(object sender, GetDataHandlers.PlaceItemFrameEven
}
}
+ /// Fired when an weapon rack is placed for anti-cheat detection.
+ /// The object that triggered the event.
+ /// The packet arguments that the event has.
+ internal void OnWeaponsRackTryPlacing(object sender, GetDataHandlers.WeaponsRackTryPlacingEventArgs args)
+ {
+ if (!TShock.Utils.TilePlacementValid(args.X, args.Y))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnWeaponsRackTryPlacing rejected tile placement valid from {0}", args.Player.Name));
+ args.Handled = true;
+ return;
+ }
+
+ if (args.Player.IsBeingDisabled())
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnWeaponsRackTryPlacing rejected disabled from {0}", args.Player.Name));
+ NetMessage.SendData((int)PacketTypes.UpdateTileEntity, -1, -1, NetworkText.Empty, args.WeaponRack.ID, 0, 1);
+ args.Handled = true;
+ return;
+ }
+
+ if (!args.Player.HasBuildPermission(args.X, args.Y))
+ {
+ int num = Item.NewItem(null, (args.X * 16) + 8, (args.Y * 16) + 8, args.Player.TPlayer.width, args.Player.TPlayer.height, args.ItemID, args.Stack, noBroadcast: true, args.Prefix, noGrabDelay: true);
+ Main.item[num].playerIndexTheItemIsReservedFor = args.Player.Index;
+ NetMessage.SendData((int)PacketTypes.ItemDrop, args.Player.Index, -1, NetworkText.Empty, num, 1f);
+ NetMessage.SendData((int)PacketTypes.ItemOwner, args.Player.Index, -1, NetworkText.Empty, num);
+
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnWeaponsRackTryPlacing rejected permissions from {0}", args.Player.Name));
+ NetMessage.SendData((int)PacketTypes.UpdateTileEntity, -1, -1, NetworkText.Empty, args.WeaponRack.ID, 0, 1);
+ args.Handled = true;
+ return;
+ }
+
+ if (!args.Player.IsInRange(args.X, args.Y))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnWeaponsRackTryPlacing rejected range checks from {0}", args.Player.Name));
+ NetMessage.SendData((int)PacketTypes.UpdateTileEntity, -1, -1, NetworkText.Empty, args.WeaponRack.ID, 0, 1);
+ args.Handled = true;
+ return;
+ }
+ }
+
internal void OnPlayerPortalTeleport(object sender, GetDataHandlers.TeleportThroughPortalEventArgs args)
{
//Packet 96 (player teleport through portal) has no validation on whether or not the player id provided
From f7c4f1ea5bd1f36c5cbcf2bb1110dcd1a8129c3c Mon Sep 17 00:00:00 2001
From: Nightklp <120942242+NightKLP@users.noreply.github.com>
Date: Fri, 13 Mar 2026 23:32:05 +0800
Subject: [PATCH 2/8] Update GetDataHandlers.cs
---
TShockAPI/GetDataHandlers.cs | 71 ++++++++++++++++++++++++++++++++++--
1 file changed, 68 insertions(+), 3 deletions(-)
diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs
index c09cf65fe..c704080b5 100644
--- a/TShockAPI/GetDataHandlers.cs
+++ b/TShockAPI/GetDataHandlers.cs
@@ -1,4 +1,4 @@
-/*
+/*
TShock, a server mod for Terraria
Copyright (C) 2011-2019 Pryaxis & TShock Contributors
@@ -16,20 +16,21 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
+using Microsoft.Xna.Framework;
using System;
+using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Streams;
using System.Linq;
-using Microsoft.Xna.Framework;
using Terraria;
using Terraria.DataStructures;
using Terraria.GameContent.Tile_Entities;
using Terraria.ID;
using Terraria.Localization;
-using TShockAPI.Models.PlayerUpdate;
using TShockAPI.Configuration;
+using TShockAPI.Models.PlayerUpdate;
namespace TShockAPI
{
@@ -136,6 +137,7 @@ public static void InitGetDataHandler()
{ PacketTypes.Emoji, HandleEmoji },
{ PacketTypes.TileEntityDisplayDollItemSync, HandleTileEntityDisplayDollItemSync },
{ PacketTypes.RequestTileEntityInteraction, HandleRequestTileEntityInteraction },
+ { PacketTypes.WeaponsRackTryPlacing, HandleWeaponsRackTryPlacing },
{ PacketTypes.SyncTilePicking, HandleSyncTilePicking },
{ PacketTypes.SyncRevengeMarker, HandleSyncRevengeMarker },
{ PacketTypes.LandGolfBallInCup, HandleLandGolfBallInCup },
@@ -2321,6 +2323,52 @@ private static bool OnRequestTileEntityInteraction(TSPlayer player, MemoryStream
RequestTileEntityInteraction.Invoke(null, args);
return args.Handled;
}
+ ///
+ /// For use in an OnWeaponsRackTryPlacing event.
+ ///
+ public class WeaponsRackTryPlacingEventArgs : GetDataHandledEventArgs
+ {
+ /// The X coordinate of the weapon rack.
+ public short X { get; set; }
+
+ /// The Y coordinate of the weapon rack.
+ public short Y { get; set; }
+
+ /// The ItemID of the weapon rack.
+ public short ItemID { get; set; }
+
+ /// The prefix.
+ public byte Prefix { get; set; }
+
+ /// The stack.
+ public short Stack { get; set; }
+
+ /// The ItemFrame object associated with this event.
+ public TEWeaponsRack WeaponRack { get; set; }
+ }
+ ///
+ /// Called when a player requests interaction with a TileEntity.
+ ///
+ public static HandlerList WeaponsRackTryPlacing = new HandlerList();
+ private static bool OnWeaponsRackTryPlacing(TSPlayer player, MemoryStream data, short x, short y, short itemID, byte prefix, short stack, TEWeaponsRack weaponRack)
+ {
+ if (WeaponsRackTryPlacing == null)
+ return false;
+
+ var args = new WeaponsRackTryPlacingEventArgs
+ {
+ Player = player,
+ Data = data,
+ X = x,
+ Y = y,
+ ItemID = itemID,
+ Prefix = prefix,
+ Stack = stack,
+ WeaponRack = weaponRack
+ };
+ WeaponsRackTryPlacing.Invoke(null, args);
+ return args.Handled;
+ }
///
/// For use in a SyncTilePicking event.
@@ -4803,6 +4851,23 @@ private static bool HandleRequestTileEntityInteraction(GetDataHandlerArgs args)
return false;
}
+ private static bool HandleWeaponsRackTryPlacing(GetDataHandlerArgs args)
+ {
+ short x = args.Data.ReadInt16();
+ short y = args.Data.ReadInt16();
+ short itemID = args.Data.ReadInt16();
+ byte prefix = args.Data.ReadInt8();
+ short stack = args.Data.ReadInt16();
+ TEWeaponsRack WeaponRack = (TEWeaponsRack)TileEntity.ByID[TEWeaponsRack.Find(x, y)];
+
+ if (OnWeaponsRackTryPlacing(args.Player, args.Data, x, y, itemID, prefix, stack, WeaponRack))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
private static bool HandleSyncTilePicking(GetDataHandlerArgs args)
{
byte playerIndex = args.Data.ReadInt8();
From 2cc4bba5f2782dfb70a041f8ca4b1259de4e3729 Mon Sep 17 00:00:00 2001
From: Nightklp <120942242+NightKLP@users.noreply.github.com>
Date: Sat, 14 Mar 2026 13:14:35 +0800
Subject: [PATCH 3/8] Update GetDataHandlers.cs
---
TShockAPI/GetDataHandlers.cs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs
index c704080b5..47df023cc 100644
--- a/TShockAPI/GetDataHandlers.cs
+++ b/TShockAPI/GetDataHandlers.cs
@@ -2346,9 +2346,7 @@ public class WeaponsRackTryPlacingEventArgs : GetDataHandledEventArgs
/// The ItemFrame object associated with this event.
public TEWeaponsRack WeaponRack { get; set; }
}
- ///
- /// Called when a player requests interaction with a TileEntity.
- ///
+ /// Fired when an WeaponRack is placed.
public static HandlerList WeaponsRackTryPlacing = new HandlerList();
private static bool OnWeaponsRackTryPlacing(TSPlayer player, MemoryStream data, short x, short y, short itemID, byte prefix, short stack, TEWeaponsRack weaponRack)
{
From 694d37860f95f733213d1a1638894e12a242cc67 Mon Sep 17 00:00:00 2001
From: Nightklp <120942242+NightKLP@users.noreply.github.com>
Date: Thu, 30 Apr 2026 12:25:15 +0800
Subject: [PATCH 4/8] Add SyncItemsWithShimmer, LeashedEntityAnchor and
SyncItemCannotBeTakenByEnemies handlers
---
TShockAPI/GetDataHandlers.cs | 139 ++++++++++++++++++++++++++++++++++-
1 file changed, 138 insertions(+), 1 deletion(-)
diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs
index 47df023cc..b786532dd 100644
--- a/TShockAPI/GetDataHandlers.cs
+++ b/TShockAPI/GetDataHandlers.cs
@@ -144,9 +144,12 @@ public static void InitGetDataHandler()
{ PacketTypes.FishOutNPC, HandleFishOutNPC },
{ PacketTypes.FoodPlatterTryPlacing, HandleFoodPlatterTryPlacing },
{ PacketTypes.SyncCavernMonsterType, HandleSyncCavernMonsterType },
+ { PacketTypes.SyncItemsWithShimmer, HandleSyncItemsWithShimmer },
{ PacketTypes.SyncLoadout, HandleSyncLoadout },
{ PacketTypes.TeamChangeFromUI, HandlePlayerTeam }, // Same packet as PlayerTeam
- { PacketTypes.TEDeadCellsDisplayJar, HandleDisplayJar }
+ { PacketTypes.SyncItemCannotBeTakenByEnemies, HandleSyncItemCannotBeTakenByEnemies },
+ { PacketTypes.TEDeadCellsDisplayJar, HandleDisplayJar },
+ { PacketTypes.TELeashedEntityAnchorPlaceItem, HandleLeashedEntityAnchorPlaceItem }
};
}
@@ -2545,6 +2548,67 @@ private static bool OnFoodPlatterTryPlacing(TSPlayer player, MemoryStream data,
FoodPlatterTryPlacing.Invoke(null, args);
return args.Handled;
}
+
+ ///
+ /// For use in an SyncItemsWithShimmer event
+ ///
+ public class SyncItemsWithShimmerEventArgs : GetDataHandledEventArgs
+ {
+ ///
+ /// ID of the item.
+ /// If below 400 and NetID(Type) is 0 Then Set Null. If ItemID is 400 Then New Item
+ ///
+ public short ID { get; set; }
+ ///
+ /// Position of the item
+ ///
+ public Vector2 Position { get; set; }
+ ///
+ /// Velocity at which the item is deployed
+ ///
+ public Vector2 Velocity { get; set; }
+ ///
+ /// Stacks
+ ///
+ public short Stacks { get; set; }
+ ///
+ /// Prefix of the item
+ ///
+ public byte Prefix { get; set; }
+ ///
+ /// No Delay on pickup
+ ///
+ public bool NoDelay { get; set; }
+ ///
+ /// Item type
+ ///
+ public short Type { get; set; }
+ }
+
+ ///
+ /// SyncItemsWithShimmer - Called when an item is sync on shimmer
+ ///
+ public static HandlerList SyncItemsWithShimmer = new HandlerList();
+ private static bool OnSyncItemsWithShimmer(TSPlayer player, MemoryStream data, short id, Vector2 pos, Vector2 vel, short stacks, byte prefix, bool noDelay, short type)
+ {
+ if (SyncItemsWithShimmer == null)
+ return false;
+
+ var args = new SyncItemsWithShimmerEventArgs
+ {
+ Player = player,
+ Data = data,
+ ID = id,
+ Position = pos,
+ Velocity = vel,
+ Stacks = stacks,
+ Prefix = prefix,
+ NoDelay = noDelay,
+ Type = type,
+ };
+ SyncItemsWithShimmer.Invoke(null, args);
+ return args.Handled;
+ }
public class DisplayJarTryPlacingEventArgs : GetDataHandledEventArgs
{
@@ -2625,7 +2689,43 @@ private static bool OnReadNetModule(TSPlayer player, MemoryStream data, NetModul
ReadNetModule.Invoke(null, args);
return args.Handled;
}
+
+ public class LeashedEntityAnchorPlaceItemEventArgs : GetDataHandledEventArgs
+ {
+ ///
+ /// The X tile position of the placement action.
+ ///
+ public ushort TileX { get; set; }
+ ///
+ /// The Y tile position of the placement action.
+ ///
+ public ushort TileY { get; set; }
+ ///
+ /// The Entity ID that is being placed in the display jar.
+ ///
+ public short EntityID { get; set; }
+ }
+ ///
+ /// Called when a player is placing an item in a Leashed Entity Anchor.
+ ///
+ public static HandlerList LeashedEntityAnchorPlaceItem = new HandlerList();
+ private static bool OnLeashedEntityAnchorPlaceItem(TSPlayer player, MemoryStream data, ushort tileX, ushort tileY, short EntityID)
+ {
+ if (LeashedEntityAnchorPlaceItem == null)
+ return false;
+ var args = new LeashedEntityAnchorPlaceItemEventArgs
+ {
+ Player = player,
+ Data = data,
+ TileX = tileX,
+ TileY = tileY,
+ EntityID = EntityID,
+ };
+ LeashedEntityAnchorPlaceItem.Invoke(null, args);
+ return args.Handled;
+ }
+
#endregion
private static bool HandlePlayerInfo(GetDataHandlerArgs args)
@@ -4941,6 +5041,22 @@ private static bool HandleSyncCavernMonsterType(GetDataHandlerArgs args)
return true;
}
+ private static bool HandleSyncItemsWithShimmer(GetDataHandlerArgs args)
+ {
+ var id = args.Data.ReadInt16();
+ var pos = new Vector2(args.Data.ReadSingle(), args.Data.ReadSingle());
+ var vel = new Vector2(args.Data.ReadSingle(), args.Data.ReadSingle());
+ var stacks = args.Data.ReadInt16();
+ var prefix = args.Data.ReadInt8();
+ var noDelay = args.Data.ReadInt8() == 1;
+ var type = args.Data.ReadInt16();
+
+ if (OnSyncItemsWithShimmer(args.Player, args.Data, id, pos, vel, stacks, prefix, noDelay, type))
+ return true;
+
+ return false;
+ }
+
private static bool HandleSyncLoadout(GetDataHandlerArgs args)
{
var playerIndex = args.Data.ReadInt8();
@@ -5029,6 +5145,13 @@ Tuple GetDyeSlotsForLoadoutIndex(int index)
return false;
}
+ private static bool HandleSyncItemCannotBeTakenByEnemies(GetDataHandlerArgs args)
+ {
+ args.Player.Kick(GetString("Exploit attempt detected!"));
+ TShock.Log.ConsoleDebug(GetString($"HandleSyncItemCannotBeTakenByEnemies: Player is trying to sync item that cannot be taken by hostile mobs; this is a crafted packet! - From {args.Player.Name}"));
+ return true;
+ }
+
private static bool HandleDisplayJar(GetDataHandlerArgs args)
{
ushort tileX = args.Data.ReadUInt16();
@@ -5043,6 +5166,20 @@ private static bool HandleDisplayJar(GetDataHandlerArgs args)
return false;
}
+ private static bool HandleLeashedEntityAnchorPlaceItem(GetDataHandlerArgs args)
+ {
+ var x = args.Data.ReadUInt16();
+ var y = args.Data.ReadUInt16();
+ var EntityID = args.Data.ReadInt16();
+ //var leashedEntityAnchor = (TELeashedEntityAnchor)TileEntity.ByID[TELeashedEntityAnchor.fi(x, y)];
+
+ if (OnLeashedEntityAnchorPlaceItem(args.Player, args.Data, x, y, EntityID))
+ {
+ return true;
+ }
+
+ return false;
+ }
public enum DoorAction
{
From 6b3cfdfca4e1298b6638d1fcef1cc250cb3f421b Mon Sep 17 00:00:00 2001
From: Nightklp <120942242+NightKLP@users.noreply.github.com>
Date: Thu, 30 Apr 2026 12:31:21 +0800
Subject: [PATCH 5/8] Add OnSyncItemsWithShimmer and
OnLeashedEntityAnchorPlaceItem handlers and missing tileEdit ( WeaponRack )
---
TShockAPI/Bouncer.cs | 199 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 199 insertions(+)
diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs
index 16e5ae96e..733108cee 100644
--- a/TShockAPI/Bouncer.cs
+++ b/TShockAPI/Bouncer.cs
@@ -138,7 +138,9 @@ internal Bouncer()
GetDataHandlers.KillMe += OnKillMe;
GetDataHandlers.FishOutNPC += OnFishOutNPC;
GetDataHandlers.FoodPlatterTryPlacing += OnFoodPlatterTryPlacing;
+ GetDataHandlers.SyncItemsWithShimmer += OnSyncItemsWithShimmer;
GetDataHandlers.DisplayJarTryPlacing += OnDisplayJarTryPlacing;
+ GetDataHandlers.LeashedEntityAnchorPlaceItem += OnLeashedEntityAnchorPlaceItem;
OTAPI.Hooks.Chest.QuickStack += OnQuickStack;
HookEvents.Terraria.Projectile.Kill_DirtAndFluidProjectiles_RunDelegateMethodPushUpForHalfBricks += OnProjectileDirtFluidKill;
HookEvents.Terraria.GameContent.CraftingRequests.CanCraftFromChest += OnChestCraftRequest;
@@ -653,6 +655,15 @@ internal void OnTileEdit(object sender, GetDataHandlers.TileEditEventArgs args)
}
}
+ if (tile.type == TileID.WeaponsRack2)
+ {
+ int weaponRackId = TEWeaponsRack.Find(tileX - tile.frameX % 36 / 18, tileY - tile.frameY % 36 / 18);
+ if (weaponRackId != -1)
+ {
+ NetMessage.SendData((int)PacketTypes.UpdateTileEntity, -1, -1, NetworkText.Empty, weaponRackId, 0, 1);
+ }
+ }
+
if (tile.type == TileID.DeadCellsDisplayJar)
{
var displayJar = TEDeadCellsDisplayJar.Find(tileX - tile.frameX % 18 / 18, tileY - tile.frameY % 32 / 18);
@@ -784,6 +795,7 @@ internal void OnTileEdit(object sender, GetDataHandlers.TileEditEventArgs args)
// also add an exception for snake coils, they can be removed when the player places a new one or after x amount of time
// If the tile is part of the breakable when placing set, it might be getting broken by a placement.
else if (tile.type != TileID.ItemFrame &&
+ tile.type != TileID.WeaponsRack2 &&
tile.type != TileID.DeadCellsDisplayJar &&
tile.type != TileID.MysticSnakeRope &&
!ItemID.Sets.Explosives[selectedItem.type] &&
@@ -3098,6 +3110,126 @@ internal void OnFoodPlatterTryPlacing(object sender, GetDataHandlers.FoodPlatter
}
}
+ /// Registered when shimmer items fall to the ground to prevent cheating.
+ /// The object that triggered the event.
+ /// The packet arguments that the event has.
+ internal void OnSyncItemsWithShimmer(object sender, GetDataHandlers.SyncItemsWithShimmerEventArgs args)
+ {
+ short id = args.ID;
+ Vector2 pos = args.Position;
+ Vector2 vel = args.Velocity;
+ short stacks = args.Stacks;
+ short prefix = args.Prefix;
+ bool noDelay = args.NoDelay;
+ short type = args.Type;
+
+ if (!float.IsFinite(pos.X) || !float.IsFinite(pos.Y))
+ {
+ TShock.Log.ConsoleInfo(GetString("Bouncer / OnSyncItemsWithShimmer force kicked (attempted to set position to infinity or NaN) from {0}", args.Player.Name));
+ args.Player.Kick(GetString("Detected DOOM set to ON position."), true, true);
+ args.Handled = true;
+ return;
+ }
+
+ if (!float.IsFinite(vel.X) || !float.IsFinite(vel.Y))
+ {
+ TShock.Log.ConsoleInfo(GetString("Bouncer / OnSyncItemsWithShimmer force kicked (attempted to set velocity to infinity or NaN) from {0}", args.Player.Name));
+ args.Player.Kick(GetString("Detected DOOM set to ON position."), true, true);
+ args.Handled = true;
+ return;
+ }
+
+ // player is attempting to crash clients
+ if (type < -48 || type >= Terraria.ID.ItemID.Count)
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemsWithShimmer rejected from attempt crash from {0}", args.Player.Name));
+ args.Handled = true;
+ return;
+ }
+
+ // make sure the prefix is a legit value
+ if (prefix > PrefixID.Count)
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemsWithShimmer rejected from prefix check from {0}", args.Player.Name));
+
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ if (type == 0)
+ {
+ if (!args.Player.IsInRange((int)(Main.item[id].position.X / 16f), (int)(Main.item[id].position.Y / 16f)))
+ {
+ // Causes item duplications. Will be re added if necessary
+ //args.Player.SendData(PacketTypes.ItemDrop, "", id);
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemsWithShimmer rejected from dupe range check from {0}", args.Player.Name));
+ args.Handled = true;
+ return;
+ }
+
+ args.Handled = false;
+ return;
+ }
+
+ if (!args.Player.IsInRange((int)(pos.X / 16f), (int)(pos.Y / 16f), 128))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemsWithShimmer rejected from range check from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ // stop the client from changing the item type of a drop
+ if (Main.item[id].active && Main.item[id].type != type &&
+ !(Main.item[id].type == ItemID.EmptyBucket && type == ItemID.WaterBucket)) // Empty bucket turns into Water Bucket on rainy days
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemsWithShimmer rejected from item drop check from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.ItemDrop, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ Item item = new Item();
+ item.netDefaults(type);
+ if ((stacks > item.maxStack || stacks <= 0) || (TShock.ItemBans.DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), args.Player) && !args.Player.HasPermission(Permissions.allowdroppingbanneditems)))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemsWithShimmer rejected from drop item ban check / max stack check / min stack check from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ // TODO: Remove item ban part of this check
+ if ((Main.ServerSideCharacter) && (DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond - args.Player.LoginMS < TShock.ServerSideCharacterConfig.Settings.LogonDiscardThreshold))
+ {
+ //Player is probably trying to sneak items onto the server in their hands!!!
+ TShock.Log.ConsoleInfo(GetString("Player {0} tried to sneak {1} onto the server!", args.Player.Name, item.Name));
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemsWithShimmer rejected from sneaky from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+
+ }
+
+ if (args.Player.IsBeingDisabled())
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemsWithShimmer rejected from disabled from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ if (type == ItemID.GuideVoodooDoll && args.Player.TPlayer.ZoneUnderworldHeight && !args.Player.HasPermission(Permissions.summonboss))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemsWithShimmer rejected Guide Voodoo Doll drop from {0}", args.Player.Name));
+ args.Player.SendErrorMessage(GetString("You do not have permission to summon the Wall of Flesh."));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+ }
+
///
/// Called when dirt/fluid projectiles (dirt bombs, liquid bombs, liquid rockets) are killed and attempt to place tiles or liquids.
///
@@ -3167,6 +3299,72 @@ internal void OnDisplayJarTryPlacing(object sender, GetDataHandlers.DisplayJarTr
return;
}
}
+
+ ///
+ /// Called when a player is trying to put an leashed entity
+ ///
+ ///
+ ///
+ internal void OnLeashedEntityAnchorPlaceItem(object sender, GetDataHandlers.LeashedEntityAnchorPlaceItemEventArgs args)
+ {
+ int tileX = args.TileX;
+ int tileY = args.TileY;
+
+ if (!TShock.Utils.TilePlacementValid(tileX, tileY))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnLeashedEntityAnchorPlaceItem rejected from (tile placement valid) {0}", args.Player.Name));
+ args.Handled = true;
+ return;
+ }
+ if (args.Player.IsBeingDisabled())
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnLeashedEntityAnchorPlaceItem rejected from disabled from {0}", args.Player.Name));
+ args.Handled = true;
+ return;
+ }
+ if (args.Player.Dead && TShock.Config.Settings.PreventDeadModification)
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnLeashedEntityAnchorPlaceItem rejected from deadmod from {0}", args.Player.Name));
+ args.Handled = true;
+ return;
+ }
+ if (!args.Player.HasBuildPermission(tileX, tileY))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnLeashedEntityAnchorPlaceItem rejected from permissions from {0}", args.Player.Name));
+ args.Handled = true;
+ return;
+ }
+ if (!args.Player.IsInRange(tileX, tileY))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnLeashedEntityAnchorPlaceItem rejected from range checks from {0}", args.Player.Name));
+ args.Player.SendTileSquareCentered(tileX, tileY, 1);
+ args.Handled = true;
+ return;
+ }
+ if (!args.Player.ItemInHand.IsAir && args.Player.ItemInHand.type != args.EntityID)
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnLeashedEntityAnchorPlaceItem rejected from item not placed by hand from {0}", args.Player.Name));
+ args.Player.SendTileSquareCentered(tileX, tileY, 1);
+ args.Handled = true;
+ return;
+ }
+ if (!args.Player.SelectedItem.IsAir && args.Player.SelectedItem.type != args.EntityID)
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnLeashedEntityAnchorPlaceItem rejected from item not selected from {0}", args.Player.Name));
+ args.Player.SendTileSquareCentered(tileX, tileY, 1);
+ args.Handled = true;
+ return;
+ }
+
+ //valid item
+ if (ItemID.Sets.PlaceTileOnAltUse[args.EntityID])
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnLeashedEntityAnchorPlaceItem rejected from invalid item alt use from {0}", args.Player.Name));
+ args.Player.SendTileSquareCentered(tileX, tileY, 1);
+ args.Handled = true;
+ return;
+ }
+ }
///
/// Called when a player is trying to put an item into chest through Quick Stack.
@@ -3339,6 +3537,7 @@ internal static int GetMaxPlaceStyle(int tileID)
TileID.Womannequin,
TileID.MinecartTrack,
TileID.WeaponsRack,
+ TileID.WeaponsRack2,
TileID.ItemFrame,
TileID.LunarMonolith,
TileID.TargetDummy,
From 0c1ca0fc0c9105db1509732c09b3461de8caa38c Mon Sep 17 00:00:00 2001
From: Nightklp <120942242+NightKLP@users.noreply.github.com>
Date: Thu, 30 Apr 2026 22:19:00 +0800
Subject: [PATCH 6/8] Add SyncItemCannotBeTakenByEnemies event handling
---
TShockAPI/GetDataHandlers.cs | 75 ++++++++++++++++++++++++++++++++++--
1 file changed, 72 insertions(+), 3 deletions(-)
diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs
index f43ca0f64..b444329d2 100644
--- a/TShockAPI/GetDataHandlers.cs
+++ b/TShockAPI/GetDataHandlers.cs
@@ -2609,6 +2609,66 @@ private static bool OnSyncItemsWithShimmer(TSPlayer player, MemoryStream data, s
SyncItemsWithShimmer.Invoke(null, args);
return args.Handled;
}
+
+ ///
+ /// For use in an SyncItemCannotBeTakenByEnemies event
+ ///
+ public class SyncItemCannotBeTakenByEnemiesEventArgs : GetDataHandledEventArgs
+ {
+ ///
+ /// ID of the item.
+ /// If below 400 and NetID(Type) is 0 Then Set Null. If ItemID is 400 Then New Item
+ ///
+ public short ID { get; set; }
+ ///
+ /// Position of the item
+ ///
+ public Vector2 Position { get; set; }
+ ///
+ /// Velocity at which the item is deployed
+ ///
+ public Vector2 Velocity { get; set; }
+ ///
+ /// Stacks
+ ///
+ public short Stacks { get; set; }
+ ///
+ /// Prefix of the item
+ ///
+ public byte Prefix { get; set; }
+ ///
+ /// No Delay on pickup
+ ///
+ public bool NoDelay { get; set; }
+ ///
+ /// Item type
+ ///
+ public short Type { get; set; }
+ }
+ ///
+ /// SyncItemCannotBeTakenByEnemies - Called when an item is dropped
+ ///
+ public static HandlerList SyncItemCannotBeTakenByEnemies = new HandlerList();
+ private static bool OnSyncItemCannotBeTakenByEnemies(TSPlayer player, MemoryStream data, short id, Vector2 pos, Vector2 vel, short stacks, byte prefix, bool noDelay, short type)
+ {
+ if (SyncItemCannotBeTakenByEnemies == null)
+ return false;
+
+ var args = new SyncItemCannotBeTakenByEnemiesEventArgs
+ {
+ Player = player,
+ Data = data,
+ ID = id,
+ Position = pos,
+ Velocity = vel,
+ Stacks = stacks,
+ Prefix = prefix,
+ NoDelay = noDelay,
+ Type = type,
+ };
+ SyncItemCannotBeTakenByEnemies.Invoke(null, args);
+ return args.Handled;
+ }
public class DisplayJarTryPlacingEventArgs : GetDataHandledEventArgs
{
@@ -5155,9 +5215,18 @@ Tuple GetDyeSlotsForLoadoutIndex(int index)
private static bool HandleSyncItemCannotBeTakenByEnemies(GetDataHandlerArgs args)
{
- args.Player.Kick(GetString("Exploit attempt detected!"));
- TShock.Log.ConsoleDebug(GetString($"HandleSyncItemCannotBeTakenByEnemies: Player is trying to sync item that cannot be taken by hostile mobs; this is a crafted packet! - From {args.Player.Name}"));
- return true;
+ var id = args.Data.ReadInt16();
+ var pos = new Vector2(args.Data.ReadSingle(), args.Data.ReadSingle());
+ var vel = new Vector2(args.Data.ReadSingle(), args.Data.ReadSingle());
+ var stacks = args.Data.ReadInt16();
+ var prefix = args.Data.ReadInt8();
+ var noDelay = args.Data.ReadInt8() == 1;
+ var type = args.Data.ReadInt16();
+
+ if (OnSyncItemCannotBeTakenByEnemies(args.Player, args.Data, id, pos, vel, stacks, prefix, noDelay, type))
+ return true;
+
+ return false;
}
private static bool HandleDisplayJar(GetDataHandlerArgs args)
From 9b9cb4a3f40233cf3dfa392e52cca5598a146e07 Mon Sep 17 00:00:00 2001
From: Nightklp <120942242+NightKLP@users.noreply.github.com>
Date: Thu, 30 Apr 2026 22:23:42 +0800
Subject: [PATCH 7/8] Implement item sync prevention from enemies
Added event handler to prevent items from being taken by enemies under certain conditions, including position and velocity checks.
---
TShockAPI/Bouncer.cs | 121 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 121 insertions(+)
diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs
index 733108cee..59ab40377 100644
--- a/TShockAPI/Bouncer.cs
+++ b/TShockAPI/Bouncer.cs
@@ -139,6 +139,7 @@ internal Bouncer()
GetDataHandlers.FishOutNPC += OnFishOutNPC;
GetDataHandlers.FoodPlatterTryPlacing += OnFoodPlatterTryPlacing;
GetDataHandlers.SyncItemsWithShimmer += OnSyncItemsWithShimmer;
+ GetDataHandlers.SyncItemCannotBeTakenByEnemies += OnSyncItemCannotBeTakenByEnemies;
GetDataHandlers.DisplayJarTryPlacing += OnDisplayJarTryPlacing;
GetDataHandlers.LeashedEntityAnchorPlaceItem += OnLeashedEntityAnchorPlaceItem;
OTAPI.Hooks.Chest.QuickStack += OnQuickStack;
@@ -3229,6 +3230,126 @@ internal void OnSyncItemsWithShimmer(object sender, GetDataHandlers.SyncItemsWit
return;
}
}
+
+ /// Registered when item items fall from enemy strike to the ground to prevent cheating.
+ /// The object that triggered the event.
+ /// The packet arguments that the event has.
+ internal void OnSyncItemCannotBeTakenByEnemies(object sender, GetDataHandlers.SyncItemCannotBeTakenByEnemiesEventArgs args)
+ {
+ short id = args.ID;
+ Vector2 pos = args.Position;
+ Vector2 vel = args.Velocity;
+ short stacks = args.Stacks;
+ short prefix = args.Prefix;
+ bool noDelay = args.NoDelay;
+ short type = args.Type;
+
+ if (!float.IsFinite(pos.X) || !float.IsFinite(pos.Y))
+ {
+ TShock.Log.ConsoleInfo(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies force kicked (attempted to set position to infinity or NaN) from {0}", args.Player.Name));
+ args.Player.Kick(GetString("Detected DOOM set to ON position."), true, true);
+ args.Handled = true;
+ return;
+ }
+
+ if (!float.IsFinite(vel.X) || !float.IsFinite(vel.Y))
+ {
+ TShock.Log.ConsoleInfo(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies force kicked (attempted to set velocity to infinity or NaN) from {0}", args.Player.Name));
+ args.Player.Kick(GetString("Detected DOOM set to ON position."), true, true);
+ args.Handled = true;
+ return;
+ }
+
+ // player is attempting to crash clients
+ if (type < -48 || type >= Terraria.ID.ItemID.Count)
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies rejected from attempt crash from {0}", args.Player.Name));
+ args.Handled = true;
+ return;
+ }
+
+ // make sure the prefix is a legit value
+ if (prefix > PrefixID.Count)
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies rejected from prefix check from {0}", args.Player.Name));
+
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ if (type == 0)
+ {
+ if (!args.Player.IsInRange((int)(Main.item[id].position.X / 16f), (int)(Main.item[id].position.Y / 16f)))
+ {
+ // Causes item duplications. Will be re added if necessary
+ //args.Player.SendData(PacketTypes.ItemDrop, "", id);
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies rejected from dupe range check from {0}", args.Player.Name));
+ args.Handled = true;
+ return;
+ }
+
+ args.Handled = false;
+ return;
+ }
+
+ if (!args.Player.IsInRange((int)(pos.X / 16f), (int)(pos.Y / 16f), 128))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies rejected from range check from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ // stop the client from changing the item type of a drop
+ if (Main.item[id].active && Main.item[id].type != type &&
+ !(Main.item[id].type == ItemID.EmptyBucket && type == ItemID.WaterBucket)) // Empty bucket turns into Water Bucket on rainy days
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies rejected from item drop check from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.ItemDrop, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ Item item = new Item();
+ item.netDefaults(type);
+ if ((stacks > item.maxStack || stacks <= 0) || (TShock.ItemBans.DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), args.Player) && !args.Player.HasPermission(Permissions.allowdroppingbanneditems)))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies rejected from drop item ban check / max stack check / min stack check from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ // TODO: Remove item ban part of this check
+ if ((Main.ServerSideCharacter) && (DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond - args.Player.LoginMS < TShock.ServerSideCharacterConfig.Settings.LogonDiscardThreshold))
+ {
+ //Player is probably trying to sneak items onto the server in their hands!!!
+ TShock.Log.ConsoleInfo(GetString("Player {0} tried to sneak {1} onto the server!", args.Player.Name, item.Name));
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies rejected from sneaky from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+
+ }
+
+ if (args.Player.IsBeingDisabled())
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies rejected from disabled from {0}", args.Player.Name));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+
+ if (type == ItemID.GuideVoodooDoll && args.Player.TPlayer.ZoneUnderworldHeight && !args.Player.HasPermission(Permissions.summonboss))
+ {
+ TShock.Log.ConsoleDebug(GetString("Bouncer / OnSyncItemCannotBeTakenByEnemies rejected Guide Voodoo Doll drop from {0}", args.Player.Name));
+ args.Player.SendErrorMessage(GetString("You do not have permission to summon the Wall of Flesh."));
+ args.Player.SendData(PacketTypes.SyncItemDespawn, "", id);
+ args.Handled = true;
+ return;
+ }
+ }
///
/// Called when dirt/fluid projectiles (dirt bombs, liquid bombs, liquid rockets) are killed and attempt to place tiles or liquids.
From 178bf50095224c94efe9192813f2792457e4bc97 Mon Sep 17 00:00:00 2001
From: Nightklp <120942242+NightKLP@users.noreply.github.com>
Date: Sat, 2 May 2026 12:07:02 +0800
Subject: [PATCH 8/8] Fix condition for valid item alt use in Bouncer
---
TShockAPI/Bouncer.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs
index 59ab40377..08d6f98ab 100644
--- a/TShockAPI/Bouncer.cs
+++ b/TShockAPI/Bouncer.cs
@@ -3478,7 +3478,7 @@ internal void OnLeashedEntityAnchorPlaceItem(object sender, GetDataHandlers.Leas
}
//valid item
- if (ItemID.Sets.PlaceTileOnAltUse[args.EntityID])
+ if (!ItemID.Sets.PlaceTileOnAltUse[args.EntityID])
{
TShock.Log.ConsoleDebug(GetString("Bouncer / OnLeashedEntityAnchorPlaceItem rejected from invalid item alt use from {0}", args.Player.Name));
args.Player.SendTileSquareCentered(tileX, tileY, 1);