diff --git a/CHANGELOG.md b/CHANGELOG.md index db04f777..619bed5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin * Move `TShock.CheckRangePermission()` to `TSPlayer.IsInRange` which **returns the opposite** of what the previous method did (see updated docs). (@hakusaro) * Move `TShock.CheckSpawn` to `Utils.IsInSpawn`. (@hakusaro) * Replace `TShock.CheckTilePermission` with `TSPlayer.HasBuildPermission`, `TSPlayer.HasPaintPermission`, and `TSPlayer.HasModifiedIceSuccessfully` respectively. (@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 d17c5266..40e2c355 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 6d7000a1..306551aa 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 a4402849..d526e851 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 06254679..c87dca0d 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -1082,17 +1082,10 @@ 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"; // 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) @@ -1753,203 +1746,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)