diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5bdcc2d..ebac456e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,7 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin
* Hooks inside TShock can now be registered with their `Register` method and can be prioritized according to the TShock HandlerList system. (@hakusaro)
* Fix message requiring login not using the command specifier set in the config file. (@hakusaro)
* Move `TShock.CheckSpawn` to `Utils.IsInSpawn`. (@hakusaro)
+* Fix stack hack detection being inconsistent between two different check points. Moved `TShock.HackedInventory` to `TSPlayer.HasHackedItemStacks`. Added `GetDataHandlers.GetDataHandledEventArgs` which is where most hooks will inherit from in the future. (@hakusaro)
## TShock 4.3.25
* Fixed a critical exploit in the Terraria protocol that could cause massive unpreventable world corruption as well as a number of other problems. Thanks to @bartico6 for reporting. Fixed by the efforts of @QuiCM, @hakusaro, and tips in the right directioon from @bartico6.
diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs
index 8e81d802..3f14e977 100644
--- a/TShockAPI/Bouncer.cs
+++ b/TShockAPI/Bouncer.cs
@@ -42,6 +42,7 @@ namespace TShockAPI
{
// Setup hooks
+ GetDataHandlers.GetSection += OnGetSection;
GetDataHandlers.PlaceItemFrame += OnPlaceItemFrame;
GetDataHandlers.GemLockToggle += OnGemLockToggle;
GetDataHandlers.PlaceTileEntity += OnPlaceTileEntity;
@@ -64,6 +65,28 @@ namespace TShockAPI
GetDataHandlers.TileEdit += OnTileEdit;
}
+ internal void OnGetSection(object sender, GetDataHandlers.GetSectionEventArgs args)
+ {
+ if (args.Player.RequestedSection)
+ {
+ args.Handled = true;
+ return;
+ }
+ args.Player.RequestedSection = true;
+
+ if (String.IsNullOrEmpty(args.Player.Name))
+ {
+ TShock.Utils.ForceKick(args.Player, "Blank name.", true);
+ args.Handled = true;
+ return;
+ }
+
+ if (!args.Player.HasPermission(Permissions.ignorestackhackdetection))
+ {
+ args.Player.IsDisabledForStackDetection = args.Player.HasHackedItemStacks(shouldWarnPlayer: true);
+ }
+ }
+
/// Fired when an item frame is placed for anti-cheat detection.
/// The object that triggered the event.
/// The packet arguments that the event has.
diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs
index a8524597..83bcb26d 100644
--- a/TShockAPI/GetDataHandlers.cs
+++ b/TShockAPI/GetDataHandlers.cs
@@ -58,6 +58,19 @@ namespace TShockAPI
}
}
+ ///
+ /// A custom HandledEventArgs that contains TShock's TSPlayer for the triggering uesr and the Terraria MP data stream.
+ /// Differentiated by GetDataHandlerArgs because it can be handled and responds to being handled.
+ ///
+ public class GetDataHandledEventArgs : HandledEventArgs
+ {
+ /// The TSPlayer that triggered the event.
+ public TSPlayer Player { get; set; }
+
+ /// The raw MP packet data associated with the event.
+ public MemoryStream Data { get; set; }
+ }
+
public static class GetDataHandlers
{
private static Dictionary GetDataHandlerDelegates;
@@ -1903,21 +1916,46 @@ namespace TShockAPI
return true;
}
+ /// The arguments to a GetSection packet.
+ public class GetSectionEventArgs : GetDataHandledEventArgs
+ {
+ /// The X position requested. Or -1 for spawn.
+ public int X { get; set; }
+
+ /// The Y position requested. Or -1 for spawn.
+ public int Y { get; set; }
+ }
+
+ /// The hook for a GetSection event.
+ public static HandlerList GetSection = new HandlerList();
+
+ /// Fires a GetSection event.
+ /// The TSPlayer that caused the GetSection.
+ /// The raw MP protocol data.
+ /// The x coordinate requested or -1 for spawn.
+ /// The y coordinate requested or -1 for spawn.
+ /// bool
+ private static bool OnGetSection(TSPlayer player, MemoryStream data, int x, int y)
+ {
+ if (GetSection == null)
+ return false;
+
+ var args = new GetSectionEventArgs
+ {
+ Player = player,
+ Data = data,
+ X = x,
+ Y = y,
+ };
+
+ GetSection.Invoke(null, args);
+ return args.Handled;
+ }
+
private static bool HandleGetSection(GetDataHandlerArgs args)
{
- if (args.Player.RequestedSection)
+ if (OnGetSection(args.Player, args.Data, args.Data.ReadInt32(), args.Data.ReadInt32()))
return true;
- args.Player.RequestedSection = true;
- if (String.IsNullOrEmpty(args.Player.Name))
- {
- TShock.Utils.ForceKick(args.Player, "Blank name.", true);
- return true;
- }
-
- if (!args.Player.HasPermission(Permissions.ignorestackhackdetection))
- {
- TShock.HackedInventory(args.Player);
- }
if (TShock.Utils.ActivePlayers() + 1 > TShock.Config.MaxSlots &&
!args.Player.HasPermission(Permissions.reservedslot))
diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs
index 69181389..89bf758c 100644
--- a/TShockAPI/TSPlayer.cs
+++ b/TShockAPI/TSPlayer.cs
@@ -307,6 +307,215 @@ namespace TShockAPI
|| !IsLoggedIn && TShock.Config.RequireLogin;
}
+ /// Checks to see if a player has hacked item stacks in their inventory, and messages them as it checks.
+ /// If the check should send a message to the player with the results of the check.
+ /// True if any stacks don't conform.
+ public bool HasHackedItemStacks(bool shouldWarnPlayer = false)
+ {
+ // Iterates through each inventory location a player has.
+ // This section is sub divided into number ranges for what each range of slots corresponds to.
+ bool check = false;
+
+ Item[] inventory = TPlayer.inventory;
+ Item[] armor = TPlayer.armor;
+ Item[] dye = TPlayer.dye;
+ Item[] miscEquips = TPlayer.miscEquips;
+ Item[] miscDyes = TPlayer.miscDyes;
+ Item[] piggy = TPlayer.bank.item;
+ Item[] safe = TPlayer.bank2.item;
+ Item[] forge = TPlayer.bank3.item;
+ Item trash = TPlayer.trashItem;
+ for (int i = 0; i < NetItem.MaxInventory; i++)
+ {
+ if (i < NetItem.InventoryIndex.Item2)
+ {
+ // From above: this is slots 0-58 in the inventory.
+ // 0-58
+ Item item = new Item();
+ if (inventory[i] != null && inventory[i].netID != 0)
+ {
+ item.netDefaults(inventory[i].netID);
+ item.Prefix(inventory[i].prefix);
+ item.AffixName();
+ if (inventory[i].stack > item.maxStack || inventory[i].stack < 0)
+ {
+ check = true;
+ if (shouldWarnPlayer)
+ {
+ SendErrorMessage("Stack cheat detected. Remove item {0} ({1}) and then rejoin.", item.Name, inventory[i].stack);
+ }
+ }
+ }
+ }
+ else if (i < NetItem.ArmorIndex.Item2)
+ {
+ // 59-78
+ var index = i - NetItem.ArmorIndex.Item1;
+ Item item = new Item();
+ if (armor[index] != null && armor[index].netID != 0)
+ {
+ item.netDefaults(armor[index].netID);
+ item.Prefix(armor[index].prefix);
+ item.AffixName();
+ if (armor[index].stack > item.maxStack || armor[index].stack < 0)
+ {
+ check = true;
+ if (shouldWarnPlayer)
+ {
+ SendErrorMessage("Stack cheat detected. Remove armor {0} ({1}) and then rejoin.", item.Name, armor[index].stack);
+ }
+ }
+ }
+ }
+ else if (i < NetItem.DyeIndex.Item2)
+ {
+ // 79-88
+ var index = i - NetItem.DyeIndex.Item1;
+ Item item = new Item();
+ if (dye[index] != null && dye[index].netID != 0)
+ {
+ item.netDefaults(dye[index].netID);
+ item.Prefix(dye[index].prefix);
+ item.AffixName();
+ if (dye[index].stack > item.maxStack || dye[index].stack < 0)
+ {
+ check = true;
+ if (shouldWarnPlayer)
+ {
+ SendErrorMessage("Stack cheat detected. Remove dye {0} ({1}) and then rejoin.", item.Name, dye[index].stack);
+ }
+ }
+ }
+ }
+ else if (i < NetItem.MiscEquipIndex.Item2)
+ {
+ // 89-93
+ var index = i - NetItem.MiscEquipIndex.Item1;
+ Item item = new Item();
+ if (miscEquips[index] != null && miscEquips[index].netID != 0)
+ {
+ item.netDefaults(miscEquips[index].netID);
+ item.Prefix(miscEquips[index].prefix);
+ item.AffixName();
+ if (miscEquips[index].stack > item.maxStack || miscEquips[index].stack < 0)
+ {
+ check = true;
+ if (shouldWarnPlayer)
+ {
+ SendErrorMessage("Stack cheat detected. Remove item {0} ({1}) and then rejoin.", item.Name, miscEquips[index].stack);
+ }
+ }
+ }
+ }
+ else if (i < NetItem.MiscDyeIndex.Item2)
+ {
+ // 93-98
+ var index = i - NetItem.MiscDyeIndex.Item1;
+ Item item = new Item();
+ if (miscDyes[index] != null && miscDyes[index].netID != 0)
+ {
+ item.netDefaults(miscDyes[index].netID);
+ item.Prefix(miscDyes[index].prefix);
+ item.AffixName();
+ if (miscDyes[index].stack > item.maxStack || miscDyes[index].stack < 0)
+ {
+ check = true;
+ if (shouldWarnPlayer)
+ {
+ SendErrorMessage("Stack cheat detected. Remove item dye {0} ({1}) and then rejoin.", item.Name, miscDyes[index].stack);
+ }
+ }
+ }
+ }
+ else if (i < NetItem.PiggyIndex.Item2)
+ {
+ // 98-138
+ var index = i - NetItem.PiggyIndex.Item1;
+ Item item = new Item();
+ if (piggy[index] != null && piggy[index].netID != 0)
+ {
+ item.netDefaults(piggy[index].netID);
+ item.Prefix(piggy[index].prefix);
+ item.AffixName();
+
+ if (piggy[index].stack > item.maxStack || piggy[index].stack < 0)
+ {
+ check = true;
+ if (shouldWarnPlayer)
+ {
+ SendErrorMessage("Stack cheat detected. Remove piggy-bank item {0} ({1}) and then rejoin.", item.Name, piggy[index].stack);
+ }
+ }
+ }
+ }
+ else if (i < NetItem.SafeIndex.Item2)
+ {
+ // 138-178
+ var index = i - NetItem.SafeIndex.Item1;
+ Item item = new Item();
+ if (safe[index] != null && safe[index].netID != 0)
+ {
+ item.netDefaults(safe[index].netID);
+ item.Prefix(safe[index].prefix);
+ item.AffixName();
+
+ if (safe[index].stack > item.maxStack || safe[index].stack < 0)
+ {
+ check = true;
+ if (shouldWarnPlayer)
+ {
+ SendErrorMessage("Stack cheat detected. Remove safe item {0} ({1}) and then rejoin.", item.Name, safe[index].stack);
+ }
+ }
+ }
+ }
+ else if (i < NetItem.TrashIndex.Item2)
+ {
+ // 179-219
+ Item item = new Item();
+ if (trash != null && trash.netID != 0)
+ {
+ item.netDefaults(trash.netID);
+ item.Prefix(trash.prefix);
+ item.AffixName();
+
+ if (trash.stack > item.maxStack)
+ {
+ check = true;
+ if (shouldWarnPlayer)
+ {
+ SendErrorMessage("Stack cheat detected. Remove trash item {0} ({1}) and then rejoin.", item.Name, trash.stack);
+ }
+ }
+ }
+ }
+ else
+ {
+ // 220
+ var index = i - NetItem.ForgeIndex.Item1;
+ Item item = new Item();
+ if (forge[index] != null && forge[index].netID != 0)
+ {
+ item.netDefaults(forge[index].netID);
+ item.Prefix(forge[index].prefix);
+ item.AffixName();
+
+ if (forge[index].stack > item.maxStack || forge[index].stack < 0)
+ {
+ check = true;
+ if (shouldWarnPlayer)
+ {
+ SendErrorMessage("Stack cheat detected. Remove Defender's Forge item {0} ({1}) and then rejoin.", item.Name, forge[index].stack);
+ }
+ }
+ }
+
+ }
+ }
+
+ return check;
+ }
+
///
/// The player's server side inventory data.
///
diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs
index 744b53df..1f8ad18a 100644
--- a/TShockAPI/TShock.cs
+++ b/TShockAPI/TShock.cs
@@ -1081,19 +1081,11 @@ namespace TShockAPI
}
else if (!Main.ServerSideCharacter || (Main.ServerSideCharacter && player.IsLoggedIn))
{
- string check = "none";
- foreach (Item item in player.TPlayer.inventory)
+ if (!player.HasPermission(Permissions.ignorestackhackdetection))
{
- if (!player.HasPermission(Permissions.ignorestackhackdetection) && (item.stack > item.maxStack || item.stack < 0) &&
- item.type != 0)
- {
- check = "Remove item " + item.Name + " (" + item.stack + ") exceeds max stack of " + item.maxStack;
- player.SendErrorMessage(check);
- break;
- }
+ player.IsDisabledForStackDetection = player.HasHackedItemStacks(shouldWarnPlayer: true);
}
- player.IsDisabledForStackDetection = true;
- check = "none";
+ string check = "none";
// Please don't remove this for the time being; without it, players wearing banned equipment will only get debuffed once
foreach (Item item in player.TPlayer.armor)
{
@@ -1147,7 +1139,8 @@ namespace TShockAPI
break;
}
}
- player.IsDisabledForBannedWearable = true;
+ if (check != "none")
+ player.IsDisabledForBannedWearable = true;
if (player.IsBeingDisabled())
{
@@ -1925,203 +1918,6 @@ namespace TShockAPI
return Utils.Distance(value1, value2);
}
- /// HackedInventory - Checks to see if a user has a hacked inventory. In addition, messages players the result.
- /// player - The TSPlayer object.
- /// bool - True if the player has a hacked inventory.
- public static bool HackedInventory(TSPlayer player)
- {
- bool check = false;
-
- Item[] inventory = player.TPlayer.inventory;
- Item[] armor = player.TPlayer.armor;
- Item[] dye = player.TPlayer.dye;
- Item[] miscEquips = player.TPlayer.miscEquips;
- Item[] miscDyes = player.TPlayer.miscDyes;
- Item[] piggy = player.TPlayer.bank.item;
- Item[] safe = player.TPlayer.bank2.item;
- Item[] forge = player.TPlayer.bank3.item;
- Item trash = player.TPlayer.trashItem;
- for (int i = 0; i < NetItem.MaxInventory; i++)
- {
- if (i < NetItem.InventoryIndex.Item2)
- {
- //0-58
- Item item = new Item();
- if (inventory[i] != null && inventory[i].netID != 0)
- {
- item.netDefaults(inventory[i].netID);
- item.Prefix(inventory[i].prefix);
- item.AffixName();
- if (inventory[i].stack > item.maxStack)
- {
- check = true;
- player.SendMessage(
- String.Format("Stack cheat detected. Remove item {0} ({1}) and then rejoin", item.Name, inventory[i].stack),
- Color.Cyan);
- }
- }
- }
- else if (i < NetItem.ArmorIndex.Item2)
- {
- //59-78
- var index = i - NetItem.ArmorIndex.Item1;
- Item item = new Item();
- if (armor[index] != null && armor[index].netID != 0)
- {
- item.netDefaults(armor[index].netID);
- item.Prefix(armor[index].prefix);
- item.AffixName();
- if (armor[index].stack > item.maxStack)
- {
- check = true;
- player.SendMessage(
- String.Format("Stack cheat detected. Remove armor {0} ({1}) and then rejoin", item.Name, armor[index].stack),
- Color.Cyan);
- }
- }
- }
- else if (i < NetItem.DyeIndex.Item2)
- {
- //79-88
- var index = i - NetItem.DyeIndex.Item1;
- Item item = new Item();
- if (dye[index] != null && dye[index].netID != 0)
- {
- item.netDefaults(dye[index].netID);
- item.Prefix(dye[index].prefix);
- item.AffixName();
- if (dye[index].stack > item.maxStack)
- {
- check = true;
- player.SendMessage(
- String.Format("Stack cheat detected. Remove dye {0} ({1}) and then rejoin", item.Name, dye[index].stack),
- Color.Cyan);
- }
- }
- }
- else if (i < NetItem.MiscEquipIndex.Item2)
- {
- //89-93
- var index = i - NetItem.MiscEquipIndex.Item1;
- Item item = new Item();
- if (miscEquips[index] != null && miscEquips[index].netID != 0)
- {
- item.netDefaults(miscEquips[index].netID);
- item.Prefix(miscEquips[index].prefix);
- item.AffixName();
- if (miscEquips[index].stack > item.maxStack)
- {
- check = true;
- player.SendMessage(
- String.Format("Stack cheat detected. Remove item {0} ({1}) and then rejoin", item.Name, miscEquips[index].stack),
- Color.Cyan);
- }
- }
- }
- else if (i < NetItem.MiscDyeIndex.Item2)
- {
- //93-98
- var index = i - NetItem.MiscDyeIndex.Item1;
- Item item = new Item();
- if (miscDyes[index] != null && miscDyes[index].netID != 0)
- {
- item.netDefaults(miscDyes[index].netID);
- item.Prefix(miscDyes[index].prefix);
- item.AffixName();
- if (miscDyes[index].stack > item.maxStack)
- {
- check = true;
- player.SendMessage(
- String.Format("Stack cheat detected. Remove item dye {0} ({1}) and then rejoin", item.Name, miscDyes[index].stack),
- Color.Cyan);
- }
- }
- }
- else if (i < NetItem.PiggyIndex.Item2)
- {
- //98-138
- var index = i - NetItem.PiggyIndex.Item1;
- Item item = new Item();
- if (piggy[index] != null && piggy[index].netID != 0)
- {
- item.netDefaults(piggy[index].netID);
- item.Prefix(piggy[index].prefix);
- item.AffixName();
-
- if (piggy[index].stack > item.maxStack)
- {
- check = true;
- player.SendMessage(
- String.Format("Stack cheat detected. Remove Piggy-bank item {0} ({1}) and then rejoin", item.Name, piggy[index].stack),
- Color.Cyan);
- }
- }
- }
- else if (i < NetItem.SafeIndex.Item2)
- {
- //138-178
- var index = i - NetItem.SafeIndex.Item1;
- Item item = new Item();
- if (safe[index] != null && safe[index].netID != 0)
- {
- item.netDefaults(safe[index].netID);
- item.Prefix(safe[index].prefix);
- item.AffixName();
-
- if (safe[index].stack > item.maxStack)
- {
- check = true;
- player.SendMessage(
- String.Format("Stack cheat detected. Remove Safe item {0} ({1}) and then rejoin", item.Name, safe[index].stack),
- Color.Cyan);
- }
- }
- }
- else if (i < NetItem.TrashIndex.Item2)
- {
- //179-219
- Item item = new Item();
- if (trash != null && trash.netID != 0)
- {
- item.netDefaults(trash.netID);
- item.Prefix(trash.prefix);
- item.AffixName();
-
- if (trash.stack > item.maxStack)
- {
- check = true;
- player.SendMessage(
- String.Format("Stack cheat detected. Remove trash item {0} ({1}) and then rejoin", item.Name, trash.stack),
- Color.Cyan);
- }
- }
- }
- else
- {
- //220
- var index = i - NetItem.ForgeIndex.Item1;
- Item item = new Item();
- if (forge[index] != null && forge[index].netID != 0)
- {
- item.netDefaults(forge[index].netID);
- item.Prefix(forge[index].prefix);
- item.AffixName();
-
- if (forge[index].stack > item.maxStack)
- {
- check = true;
- player.SendMessage(
- String.Format("Stack cheat detected. Remove Defender's Forge item {0} ({1}) and then rejoin", item.Name, forge[index].stack),
- Color.Cyan);
- }
- }
-
- }
- }
-
- return check;
- }
-
/// OnConfigRead - Fired when the config file has been read.
/// file - The config file object.
public void OnConfigRead(ConfigFile file)