diff --git a/CHANGELOG.md b/CHANGELOG.md index e2c4f47c..7a25311e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin * Added `GetDataHandlers.PlaceObject` hook. (@hakusaro) * `GetDataHandlers.KillMe` now sends a `TSPlayer` and a `PlayerDeathReason`. (@hakusaro) * Added `GetDataHandlers.ProjectileKill` hook. (@hakusaro) -* Removed `TShock.CheckProjectilePermission` and replaced it with `TSPlayer.HasProjectilePermission` and `TSPlayer.LacksProjectilePermission` respectively. (@hakusaro) +* Removed `TShock.CheckProjectilePermission`. (@hakusaro) * Added `TSPlayer` object to `GetDataHandlers.LiquidSetEventArgs`. (@hakusaro) * Removed `TShock.StartInvasion` for public use (moved to Utils and marked internal). (@hakusaro) * Fixed invasions started by TShock not reporting size correctly and probably not working at all. (@hakusaro) @@ -90,7 +90,7 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin * `Utils.TryParseTime` can now take spaces (e.g., `3d 5h 2m 3s`) (@QuiCM) * Enabled banning unregistered users (@QuiCM) * Added filtering and validation on packet 96 (Teleport player through portal) (@QuiCM) -* Update tracker now uses TLS (@pandabear41) +* Update tracker now uses TLS (@pandabear41) * When deleting an user account, any player logged in to that account is now logged out properly (@Enerdy) * Add NPCAddBuff data handler and bouncer (@AxeelAnder) * Improved config file documentation (@Enerdy) @@ -98,6 +98,7 @@ This is the rolling changelog for TShock for Terraria. Use past tense when addin * Update sqlite binaries to 32bit 3.27.2 for Windows (@hakusaro) * Fix banned armour checks not clearing properly (thanks @tysonstrange) * Added warning message on invalid group comand (@hakusaro, thanks to IcyPhoenix, nuLLzy & Cy on Discord) +* Moved item bans subsystem to isolated file/contained mini-plugin & reorganized codebase accordingly. (@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 7b253ed4..f55c7aaa 100644 --- a/TShockAPI/Bouncer.cs +++ b/TShockAPI/Bouncer.cs @@ -757,7 +757,7 @@ namespace TShockAPI return; } } - + /// Bouncer's projectile trigger hook stops world damaging projectiles from destroying the world. /// The object that triggered the event. /// The packet arguments that the event has. @@ -803,8 +803,25 @@ namespace TShockAPI return; } - bool hasPermission = args.Player.HasProjectilePermission(index, type); - if (!TShock.Config.IgnoreProjUpdate && !hasPermission && !args.Player.HasPermission(Permissions.ignoreprojectiledetection)) + // Main.projHostile contains projectiles that can harm players + // without PvP enabled and belong to enemy mobs, so they shouldn't be + // possible for players to create. (Source: Ijwu, QuiCM) + if (Main.projHostile[type]) + { + args.Player.RemoveProjectile(ident, owner); + args.Handled = true; + return; + } + + // Tombstones should never be permitted by players + if (type == ProjectileID.Tombstone) + { + args.Player.RemoveProjectile(ident, owner); + args.Handled = true; + return; + } + + if (!TShock.Config.IgnoreProjUpdate && !args.Player.HasPermission(Permissions.ignoreprojectiledetection)) { if (type == ProjectileID.BlowupSmokeMoonlord || type == ProjectileID.PhantasmalEye @@ -856,8 +873,7 @@ namespace TShockAPI } } - if (hasPermission && - (type == ProjectileID.Bomb + if ((type == ProjectileID.Bomb || type == ProjectileID.Dynamite || type == ProjectileID.StickyBomb || type == ProjectileID.StickyDynamite)) @@ -866,7 +882,7 @@ namespace TShockAPI args.Player.RecentFuse = 10; } } - + /// Handles the NPC Strike event for Bouncer. /// The object that triggered the event. /// The packet arguments that the event has. diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index f8083b60..3af965a8 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -3274,7 +3274,32 @@ namespace TShockAPI } else { - TShock.Itembans.AddNewBan(EnglishLanguage.GetItemNameById(items[0].type)); + // Yes this is required because of localization + // User may have passed in localized name but itembans works on English names + string englishNameForStorage = EnglishLanguage.GetItemNameById(items[0].type); + TShock.Itembans.AddNewBan(englishNameForStorage); + + // It was decided in Telegram that we would continue to ban + // projectiles based on whether or not their associated item was + // banned. However, it was also decided that we'd change the way + // this worked: in particular, we'd make it so that the item ban + // system just adds things to the projectile ban system at the + // command layer instead of inferring the state of projectile + // bans based on the state of the item ban system. + + if (items[0].type == ItemID.DirtRod) + { + TShock.ProjectileBans.AddNewBan(ProjectileID.DirtBall); + } + + if (items[0].type == ItemID.Sandgun) + { + TShock.ProjectileBans.AddNewBan(ProjectileID.SandBallGun); + TShock.ProjectileBans.AddNewBan(ProjectileID.EbonsandBallGun); + TShock.ProjectileBans.AddNewBan(ProjectileID.PearlSandBallGun); + } + + // This returns the localized name to the player, not the item as it was stored. args.Player.SendSuccessMessage("Banned " + items[0].Name + "."); } } diff --git a/TShockAPI/GetDataHandlers.cs b/TShockAPI/GetDataHandlers.cs index e663af32..64242a07 100644 --- a/TShockAPI/GetDataHandlers.cs +++ b/TShockAPI/GetDataHandlers.cs @@ -344,7 +344,7 @@ namespace TShockAPI PlayerUpdate.Invoke(null, args); return args.Handled; } - + /// /// For use in a PlayerHP event /// @@ -767,7 +767,7 @@ namespace TShockAPI PlayerSpawn.Invoke(null, args); return args.Handled; } - + /// /// For use in a ChestItemChange event /// @@ -816,7 +816,7 @@ namespace TShockAPI ChestItemChange.Invoke(null, args); return args.Handled; } - + /// /// For use with a ChestOpen event /// @@ -2054,15 +2054,6 @@ namespace TShockAPI if (control[5]) { - // ItemBan system - string itemName = args.TPlayer.inventory[item].Name; - if (TShock.Itembans.ItemIsBanned(EnglishLanguage.GetItemNameById(args.TPlayer.inventory[item].netID), args.Player)) - { - control[5] = false; - args.Player.Disable("using a banned item ({0})".SFormat(itemName), DisableFlags.WriteToLogAndConsole); - args.Player.SendErrorMessage("You cannot use {0} on this server. Your actions are being ignored.", itemName); - } - // Reimplementation of normal Terraria stuff? if (args.TPlayer.inventory[item].Name == "Mana Crystal" && args.Player.TPlayer.statManaMax <= 180) { @@ -2364,12 +2355,24 @@ namespace TShockAPI return true; } - var type = Main.projectile[index].type; + short type = (short) Main.projectile[index].type; // TODO: This needs to be moved somewhere else. - if (!args.Player.HasProjectilePermission(index, type) && type != 102 && type != 100 && !TShock.Config.IgnoreProjKill) + + if (type == ProjectileID.Tombstone) { - args.Player.Disable("Does not have projectile permission to kill projectile.", DisableFlags.WriteToLogAndConsole); + args.Player.RemoveProjectile(ident, owner); + return true; + } + + if (TShock.ProjectileBans.ProjectileIsBanned(type, args.Player) && !TShock.Config.IgnoreProjKill) + { + // According to 2012 deathmax, this is a workaround to fix skeletron prime issues + // https://github.com/Pryaxis/TShock/commit/a5aa9231239926f361b7246651e32144bbf28dda + if (type == ProjectileID.Bomb || type == ProjectileID.DeathLaser) + { + return false; + } args.Player.RemoveProjectile(ident, owner); return true; } @@ -2424,7 +2427,7 @@ namespace TShockAPI Item item = new Item(); item.netDefaults(type); - if (stacks > item.maxStack || TShock.Itembans.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), args.Player)) + if (stacks > item.maxStack) { return true; } diff --git a/TShockAPI/ItemBans.cs b/TShockAPI/ItemBans.cs new file mode 100644 index 00000000..b719db9c --- /dev/null +++ b/TShockAPI/ItemBans.cs @@ -0,0 +1,216 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2011-2018 Pryaxis & TShock Contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +using System; +using System.Collections.Generic; +using System.Linq; +using Terraria.ID; +using TShockAPI.DB; +using TShockAPI.Net; +using Terraria; +using Microsoft.Xna.Framework; +using OTAPI.Tile; +using TShockAPI.Localization; +using static TShockAPI.GetDataHandlers; +using TerrariaApi.Server; +using Terraria.ObjectData; +using Terraria.DataStructures; +using Terraria.Localization; +using System.Data; + +namespace TShockAPI +{ + /// The TShock item ban subsystem. It handles keeping things out of people's inventories. + internal sealed class ItemBans + { + + /// The database connection layer to for the item ban subsystem. + private ItemManager DataModel; + + /// The last time the second update process was run. Used to throttle task execution. + private DateTime LastTimelyRun = DateTime.UtcNow; + + /// A reference to the TShock plugin so we can register events. + private TShock Plugin; + + /// Creates an ItemBan system given a plugin to register events to and a database. + /// The executing plugin. + /// The database the item ban information is stored in. + /// A new item ban system. + internal ItemBans(TShock plugin, IDbConnection database) + { + DataModel = new ItemManager(database); + Plugin = plugin; + + ServerApi.Hooks.GameUpdate.Register(plugin, OnGameUpdate); + GetDataHandlers.PlayerUpdate += OnPlayerUpdate; + GetDataHandlers.ChestItemChange += OnChestItemChange; + } + + /// Called on the game update loop (the XNA tickrate). + /// The standard event arguments. + internal void OnGameUpdate(EventArgs args) + { + if ((DateTime.UtcNow - LastTimelyRun).TotalSeconds >= 1) + { + OnSecondlyUpdate(args); + } + } + + /// Called by OnGameUpdate once per second to execute tasks regularly but not too often. + /// The standard event arguments. + internal void OnSecondlyUpdate(EventArgs args) + { + DisableFlags disableFlags = TShock.Config.DisableSecondUpdateLogs ? DisableFlags.WriteToConsole : DisableFlags.WriteToLogAndConsole; + + foreach (TSPlayer player in TShock.Players) + { + if (player == null || !player.Active) + { + continue; + } + + // Untaint now, re-taint if they fail the check. + UnTaint(player); + + // No matter the player type, we do a check when a player is holding an item that's banned. + if (DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(player.TPlayer.inventory[player.TPlayer.selectedItem].netID), player)) + { + string itemName = player.TPlayer.inventory[player.TPlayer.selectedItem].Name; + player.Disable($"holding banned item: {itemName}", disableFlags); + SendCorrectiveMessage(player, itemName); + } + + // If SSC isn't enabled OR if SSC is enabled and the player is logged in + // In a case like this, we do the full check too. + if (!Main.ServerSideCharacter || (Main.ServerSideCharacter && player.IsLoggedIn)) + { + // The Terraria inventory is composed of a multicultural set of arrays + // with various different contents and beliefs + + // Armor ban checks + foreach (Item item in player.TPlayer.armor) + { + if (DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), player)) + { + Taint(player); + SendCorrectiveMessage(player, item.Name); + } + } + + // Dye ban checks + foreach (Item item in player.TPlayer.dye) + { + if (DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), player)) + { + Taint(player); + SendCorrectiveMessage(player, item.Name); + } + } + + // Misc equip ban checks + foreach (Item item in player.TPlayer.miscEquips) + { + if (DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), player)) + { + Taint(player); + SendCorrectiveMessage(player, item.Name); + } + } + + // Misc dye ban checks + foreach (Item item in player.TPlayer.miscDyes) + { + if (DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), player)) + { + Taint(player); + SendCorrectiveMessage(player, item.Name); + } + } + } + } + + // Set the update time to now, so that we know when to execute next. + // We do this at the end so that the task can't re-execute faster than we expected. + // (If we did this at the start of the method, the method execution would count towards the timer.) + LastTimelyRun = DateTime.UtcNow; + } + + internal void OnPlayerUpdate(object sender, PlayerUpdateEventArgs args) + { + DisableFlags disableFlags = TShock.Config.DisableSecondUpdateLogs ? DisableFlags.WriteToConsole : DisableFlags.WriteToLogAndConsole; + bool useItem = ((BitsByte) args.Control)[5]; + TSPlayer player = args.Player; + string itemName = player.TPlayer.inventory[args.Item].Name; + + if (DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(player.TPlayer.inventory[args.Item].netID), args.Player)) + { + player.TPlayer.controlUseItem = false; + player.Disable($"holding banned item: {itemName}", disableFlags); + + SendCorrectiveMessage(player, itemName); + + player.TPlayer.Update(player.TPlayer.whoAmI); + NetMessage.SendData((int)PacketTypes.PlayerUpdate, -1, player.Index, NetworkText.Empty, player.Index); + + args.Handled = true; + return; + } + + args.Handled = false; + return; + } + + internal void OnChestItemChange(object sender, ChestItemEventArgs args) + { + Item item = new Item(); + item.netDefaults(args.Type); + + + if (DataModel.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), args.Player)) + { + SendCorrectiveMessage(args.Player, item.Name); + args.Handled = true; + return; + } + + args.Handled = false; + return; + } + + private void UnTaint(TSPlayer player) + { + player.IsDisabledForBannedWearable = false; + } + + private void Taint(TSPlayer player) + { + // Arbitrarily does things to the player + player.SetBuff(BuffID.Frozen, 330, true); + player.SetBuff(BuffID.Stoned, 330, true); + player.SetBuff(BuffID.Webbed, 330, true); + + // Marks them as a target for future disables + player.IsDisabledForBannedWearable = true; + } + + private void SendCorrectiveMessage(TSPlayer player, string itemName) + { + player.SendErrorMessage("{0} is banned! Remove it!", itemName); + } + } +} diff --git a/TShockAPI/TSPlayer.cs b/TShockAPI/TSPlayer.cs index 9bcdd0b9..5829f877 100644 --- a/TShockAPI/TSPlayer.cs +++ b/TShockAPI/TSPlayer.cs @@ -1173,48 +1173,6 @@ namespace TShockAPI } } - /// Checks to see if this player object has access rights to a given projectile. Used by projectile bans. - /// The projectile index from Main.projectiles (NOT from a packet directly). - /// The type of projectile, from Main.projectiles. - /// If the player has access rights to the projectile. - public bool HasProjectilePermission(int index, int type) - { - // Players never have the rights to tombstones. - if (type == ProjectileID.Tombstone) - { - return false; - } - - // Dirt balls are the projectiles from dirt rods. - // If the dirt rod item is banned, they probably shouldn't have this projectile. - if (type == ProjectileID.DirtBall && TShock.Itembans.ItemIsBanned("Dirt Rod", this)) - { - return false; - } - - // If the sandgun is banned, block sand bullets. - if (TShock.Itembans.ItemIsBanned("Sandgun", this)) - { - if (type == ProjectileID.SandBallGun - || type == ProjectileID.EbonsandBallGun - || type == ProjectileID.PearlSandBallGun) - { - return false; - } - } - - // If the projectile is hostile, block it? - Projectile tempProjectile = new Projectile(); - tempProjectile.SetDefaults(type); - - if (Main.projHostile[type]) - { - return false; - } - - return true; - } - /// /// Removes the projectile with the given index and owner. /// diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index bf662d38..bb33bcc9 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -134,6 +134,9 @@ namespace TShockAPI /// The TShock anti-cheat/anti-exploit system. internal Bouncer Bouncer; + /// The TShock item ban system. + internal ItemBans ItemBans; + /// /// TShock's Region subsystem. /// @@ -325,6 +328,7 @@ namespace TShockAPI RestManager.RegisterRestfulCommands(); Bouncer = new Bouncer(); RegionSystem = new RegionHandler(Regions); + ItemBans = new ItemBans(this, DB); var geoippath = "GeoIP.dat"; if (Config.EnableGeoIP && File.Exists(geoippath)) @@ -1056,92 +1060,17 @@ namespace TShockAPI player.Spawn(); } - if (Main.ServerSideCharacter && !player.IsLoggedIn) - { - if (player.IsBeingDisabled()) - { - player.Disable(flags: flags); - } - else if (Itembans.ItemIsBanned(EnglishLanguage.GetItemNameById(player.TPlayer.inventory[player.TPlayer.selectedItem].netID), player)) - { - player.Disable($"holding banned item: {player.TPlayer.inventory[player.TPlayer.selectedItem].Name}", flags); - player.SendErrorMessage($"You are holding a banned item: {player.TPlayer.inventory[player.TPlayer.selectedItem].Name}"); - } - } - else if (!Main.ServerSideCharacter || (Main.ServerSideCharacter && player.IsLoggedIn)) + if (!Main.ServerSideCharacter || (Main.ServerSideCharacter && player.IsLoggedIn)) { if (!player.HasPermission(Permissions.ignorestackhackdetection)) { player.IsDisabledForStackDetection = player.HasHackedItemStacks(shouldWarnPlayer: true); } - 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) - { - if (Itembans.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), player)) - { - player.SetBuff(BuffID.Frozen, 330, true); - player.SetBuff(BuffID.Stoned, 330, true); - player.SetBuff(BuffID.Webbed, 330, true); - check = "Remove armor/accessory " + item.Name; - - player.SendErrorMessage("You are wearing banned equipment. {0}", check); - break; - } - } - foreach (Item item in player.TPlayer.dye) - { - if (Itembans.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), player)) - { - player.SetBuff(BuffID.Frozen, 330, true); - player.SetBuff(BuffID.Stoned, 330, true); - player.SetBuff(BuffID.Webbed, 330, true); - check = "Remove dye " + item.Name; - - player.SendErrorMessage("You are wearing banned equipment. {0}", check); - break; - } - } - foreach (Item item in player.TPlayer.miscEquips) - { - if (Itembans.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), player)) - { - player.SetBuff(BuffID.Frozen, 330, true); - player.SetBuff(BuffID.Stoned, 330, true); - player.SetBuff(BuffID.Webbed, 330, true); - check = "Remove misc equip " + item.Name; - - player.SendErrorMessage("You are wearing banned equipment. {0}", check); - break; - } - } - foreach (Item item in player.TPlayer.miscDyes) - { - if (Itembans.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), player)) - { - player.SetBuff(BuffID.Frozen, 330, true); - player.SetBuff(BuffID.Stoned, 330, true); - player.SetBuff(BuffID.Webbed, 330, true); - check = "Remove misc dye " + item.Name; - - player.SendErrorMessage("You are wearing banned equipment. {0}", check); - break; - } - } - if (check != "none") - player.IsDisabledForBannedWearable = true; - else - player.IsDisabledForBannedWearable = false; if (player.IsBeingDisabled()) { player.Disable(flags: flags); } - else if (Itembans.ItemIsBanned(EnglishLanguage.GetItemNameById(player.TPlayer.inventory[player.TPlayer.selectedItem].netID), player)) - { - player.Disable($"holding banned item: {player.TPlayer.inventory[player.TPlayer.selectedItem].Name}", flags); - player.SendErrorMessage($"You are holding a banned item: {player.TPlayer.inventory[player.TPlayer.selectedItem].Name}"); - } } } } diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index b3deaaea..27c812c1 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -39,7 +39,6 @@ bin\Debug\TShockAPI.XML x86 false - pdbonly @@ -133,6 +132,7 @@ + True True