From b5f95d59187a3b9775a7f71bfe7f18a023f2f56d Mon Sep 17 00:00:00 2001 From: Lucas Nicodemus Date: Sat, 16 May 2020 16:27:34 -0700 Subject: [PATCH] Fragments: Separate out item bans (#1595) * Remove commented out warning disable * Add initial ItemBans segregation infrastructure * Add shell for initial OnSecondUpdate stuff * Add comments yo * Remove duplicated logic * Split out more item ban code This part of the fragments work is primarily aimed at reducing the complexity of OnSecondUpdate in TShock and moving that check out into the ItemBans subsytem. Of major note in this is the removal of "check", which was a string variable that tracked state and replacement of many of the item ban activities with sane private methods that are at least somewhat sensible. Obviously there's a lot to be desired in this system and I'm really going for a run here by trying to continue a branch from so long ago that I barely even remember the whole point of existence. Still to do: GetDataHandlers related item ban code needs to be moved into its own hook in the ItemBan system. Finally, there is a downside to some of this: we're basically iterating over players again and again if we keep this pattern up, which is kinda lame for complexity purposes. * alt j: comment changes * Move item ban check out of main playerupdate check Separates out item ban logic from the rest of GetDataHandlers so that item bans is more isolated in terms of what fragments is asking for. * alt-j: convert indentation to tabs * alt-j: fix botching source code * Move item ban related chest checks out of gdh * Remove chest item change detection from item bans It doesn't do anything. If a user removes an item from a chest, it bypasses this check. If a user adds an item to a chest, the server seems to persist the change anyway, even if the event is handled. That's a bug for sure, but fundamentally, it's not the item ban system's fault. * Revert "Remove chest item change detection from item bans" This reverts commit 758541ac5c4d4096df2db05ba2a398968113e1e4. * Fix logic issues related to item ban handling Re-implements chest item handling and correctly handles events and returns after setting handled event state. * Remove TSPlayer.HasProjectilePermission In infinite wisdom, it turns out this is not a good method for TSPlayer to have. It just checks the states of things as per what the item ban system says is banned and then creates implicit relationships to the projectile ban system. Doing this effectively knocks down another external reference to the item ban system outside of the context of the implementation for the system itself and its related hooks. This commit also adds context around what the heck is going on with some of our more interesting checks as per discussions in Telegram with @Ijwu and @QuiCM. * Update changelog * Remove useless ref to Projectile.SetDefaults * Change item ban to ban based on ID not strings I think I was so confused as to why we were passing strings everywhere that I just felt inclined to continue the trend in previous commits. --- CHANGELOG.md | 5 +- TShockAPI/Bouncer.cs | 28 ++++- TShockAPI/Commands.cs | 27 ++++- TShockAPI/GetDataHandlers.cs | 35 +++--- TShockAPI/ItemBans.cs | 216 +++++++++++++++++++++++++++++++++++ TShockAPI/TSPlayer.cs | 42 ------- TShockAPI/TShock.cs | 81 +------------ TShockAPI/TShockAPI.csproj | 2 +- 8 files changed, 292 insertions(+), 144 deletions(-) create mode 100644 TShockAPI/ItemBans.cs 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