diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 00000000..6f9e5960 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,57 @@ +{ + "files": [ + "README.md", + "README_cn.md" + ], + "imageSize": 100, + "commit": false, + "contributors": [ + { + "login": "AviKav", + "name": "AviKav", + "avatar_url": "https://avatars2.githubusercontent.com/u/18518861?v=4", + "profile": "https://avikav.net", + "contributions": [ + "bug", + "test" + ] + }, + { + "login": "AxisKriel", + "name": "Rodrigo Rente", + "avatar_url": "https://avatars0.githubusercontent.com/u/3332657?v=4", + "profile": "https://tshock.co", + "contributions": [ + "code", + "projectManagement", + "test" + ] + }, + { + "login": "sgkoishi", + "name": "Stargazing Koishi", + "avatar_url": "https://avatars2.githubusercontent.com/u/9637711?v=4", + "profile": "https://sgkoi.dev", + "contributions": [ + "code", + "infra" + ] + }, + { + "login": "AxeelAnder", + "name": "Axeel", + "avatar_url": "https://avatars2.githubusercontent.com/u/25691207?v=4", + "profile": "https://github.com/AxeelAnder", + "contributions": [ + "doc", + "projectManagement" + ] + } + ], + "contributorsPerLine": 7, + "projectName": "TShock", + "projectOwner": "Pryaxis", + "repoType": "github", + "repoHost": "https://github.com", + "skipCi": true +} diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..990b9ed1 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +> By participating in the TShock for Terraria community, all members will adhere to maintaining decorum with respect to all humans, in and out of the community. Members will not engage in discussion that inappropriately disparages or marginalizes any group of people or any individual. Members will not attempt to further or advance an agenda to the point of being overbearing or close minded (such as through spreading FUD). Members will not abuse services provided to them and will follow the guidance of community leaders on a situational basis about what abuse consists of. Members will adhere to United States and international law. If members notice a violation of this code of conduct, they will not engage but will instead contact the leadership team on either the forums or Discord. + +> Do not attempt to circumvent or bypass the code of conduct by using clever logic or reasoning (e.g., insulting Facepunch members, because they weren't directly mentioned here). \ No newline at end of file diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 75% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index 6382cc88..b28f0c83 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -8,38 +8,26 @@ Please follow these simple requirements before posting a bug report: - How to reproduce the issue - Screenshots of the issue (if applicable) -### To build the source - -Note: This includes the API by default. If you need only the API, you need to cd into that folder and do the following with the .sln file for the API. For those new to C#, the .sln and .csproj files contain the necessary definitions to do a complete source build using Microsoft or Mono build tools. - -- Checkout the source. -- Initialize the submodules: ```git submodule update --init``` -- Open the source in your favorite text editor that supports .NET building and press the build button OR -- Run ```msbuild TShock.sln``` in the root of the cloned folder on Windows in a 'Developer Command Prompt' OR -- Run ```xbuild TShock.sln``` in the root of the cloned folder on Unix. - -Need help? Drop by Slack and we'll be happy to explain it with more words, step by step. - ### TShock Additions If something is better suited to be a plugin for TShock, rather than a TShock core feature, it should not be added! Project scope is at times questionable, though, so create an issue on Github for discussion first. If an issue is completely outside of the scope of TShock, it will be made clear in that issue what it is. _If you are confused, make a suggestion. We will determine scope and relevance for you._ -_If a person makes a suggestion in Slack, capture the suggestion as a Github issue. If a suggestion crops up on the forums, make a Github issue to capture it. If you want, direct the user to make a suggestion on Github, but set an alarm/timer/reminder so that if they don't know how to use Github or they don't have an account, an issue is still made and discussed. Make it clear that the issue is a surrogate issue for a suggestion from Slack/the forums too._ +_If a person makes a suggestion in Discord, capture the suggestion as a Github issue. If a suggestion crops up on the forums, make a Github issue to capture it. If you want, direct the user to make a suggestion on Github, but set an alarm/timer/reminder so that if they don't know how to use Github or they don't have an account, an issue is still made and discussed. Make it clear that the issue is a surrogate issue for a suggestion from Discord/the forums too._ ### Pull Request Dev Guidelines These guidelines are for all contributors. * Create an issue first to suggest an improvement or feature addition to TShock. -* Active developers will then give a go/no go for implementation. This is scope related: if an issue is within the scope of TShock, it will be tagged 'pr-wanted.' -* After 'pr-wanted' has been added, an issue should be considered workable in a pull request fashion. -* If you, as a developer, want to claim an issue for a PR, as soon as possible start work and note that in both the original issue and the new PR. The 'pr-wanted' tag will remain but the active PR will become the center for discussion for your implementation. +* Active developers will then give a go/no go for implementation. This is scope related: if an issue is within the scope of TShock, it will be tagged 'Contribution Wanted.' +* After 'Contribution Wanted' has been added, an issue should be considered workable in a pull request fashion. +* If you, as a developer, want to claim an issue for a PR, as soon as possible start work and note that in both the original issue and the new PR. The 'Contribution Wanted' tag will remain but the active PR will become the center for discussion for your implementation. * If a TShock core developer takes an issue, they'll be assigned to the issue. If your issue was taken by a TShock developer and you were actively developing it in a PR, you should _make it clear as soon as possible that a process error has been made_ so that the your development resources and our development resources aren't wasted. * Please send a pull request with at least a sentence description and something meaningful as the title, not just the issue number you're fixing. -_The pr-wanted tag indicates an issue should be implemented. If an issue has a developer assigned, it indicates that they're working on it. When in doubt, ask where an issue is before starting work (so you don't waste time)!_ +_The tag indicates an issue should be implemented. If an issue has a developer assigned, it indicates that they're working on it. When in doubt, ask where an issue is before starting work (so you don't waste time)!_ Even if you have write access to the repository, follow [Github flow](https://guides.github.com/introduction/flow/) when sending commits. Don't send commits directly to either ```master``` or ```general-devel``` unless those commits modify either the deploy scripts or non-code components. If it compiles, follow Github Flow. @@ -56,6 +44,7 @@ Required: - When using static methods on primitives, use the CLR type. E.g. ```String.Format``` instead of ```string.Format```. - Always use properties, not public fields. - Document deprecations and fail compilation if they're included with ```[Obsolete("Use blah instead of blahx...", true)]```. +- Update the `CHANGELOG.md` file. ### Dev Team Guidelines diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..32f48df7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +custom: https://www.givedirectly.org/ diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..5c4cc69f --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,23 @@ +!!!!!! PLEASE FILL IN THE TEMPLATE BELOW THANK YOU VERY MUCH !!!!!! + +If you don't need help, delete this template and just post an issue (feature requests and discussions and the like). + +* TShock version: +* TShock build number (if known): + +#### Reproduction steps (if applicable)? + +1. Some step +2. Some other step +3. Some bigger step +4. This is the problem + +#### Any stack traces or error messages (if known)? + +``` +PUT SUPER LONG ERROR MESSAGES IN THE TICK MARKS +``` + +#### Any screenshots? + +!!!!!! PLEASE FILL IN THE TEMPLATE ABOVE THANK YOU VERY MUCH !!!!!! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..5f7e8ece --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Get help with TShock + url: https://github.com/Pryaxis/TShock/discussions/new + about: If you aren't sure if you have a bug, or if you're having any problems with TShock, post a question here. + - name: Chat with people on Discord + url: https://discord.gg/Cav9nYX + about: If you wanna chill or brainstorm, here's the place to do that. + - name: Don't talk about TShock + url: https://t.me/refugeecamp + about: Where the TShock developers chat about nothing related to TShock. + - name: 在QQ群提问 + url: https://jq.qq.com/?_wv=1027&k=5GJZCe4 + about: 如果你不会英语或想使用中文进行交流,可以加入我们的官方QQ群 diff --git a/.github/ISSUE_TEMPLATE/defect-report.md b/.github/ISSUE_TEMPLATE/defect-report.md new file mode 100644 index 00000000..0ebbbf14 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/defect-report.md @@ -0,0 +1,32 @@ +--- +name: Defect report +about: Report a software defect in TShock (a problem you know is our fault) +title: '' +labels: '' +assignees: '' + +--- + + + +* TShock version: +* TShock build number (if known): + +#### Reproduction steps (if applicable)? + +1. Some step +2. Some other step +3. Some bigger step +4. This is the problem + +#### Any stack traces or error messages (if known)? + +``` +PUT SUPER LONG ERROR MESSAGES IN THE TICK MARKS +``` + +#### Any screenshots? + +#### Any log messages from files that end in `.log`? + +#### What plugins and what versions of those plugins are you running? diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..28865d5c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest a change to be made to TShock +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4cf87d10 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ + + +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? +?????? HAVE YOU UPDATED THE CHANGELOG? ?????? \ No newline at end of file diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..34b33fce --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,10 @@ +updateDocsComment: > + Thanks for the pull request! TShock's maintainers would like you to go ahead and give yourself credit by updating the `CHANGELOG.md` as soon as you can. Your pull request will likely not be accepted without this. This both helps us document changes to TShock, as well as give you credit for your work. You deserve it, so go take credit! :sparkles: + +updateDocsWhiteList: + - bug + - chore + +updateDocsTargetFiles: + - README + - CHANGELOG.md diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 00000000..e85de284 --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,13 @@ +# Configuration for probot-no-response - https://github.com/probot/no-response + +# Number of days of inactivity before an Issue is closed for lack of response +daysUntilClose: 7 +# Label requiring a response +responseRequiredLabel: followup-required +# Comment to post when closing an Issue for lack of response. Set to `false` to disable +closeComment: > + This issue has been automatically closed because there has been no response + to our request for more information from the original author. With only the + information that is currently in the issue, we don't have enough information + to take action. Please reach out if you have or find the answers we need so + that we can investigate further. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..8c96f6cb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,63 @@ +name: Build Server + +on: [push] + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v1 + with: + submodules: recursive + - name: Install nuget + run: choco install nuget.commandline + - name: OTAPI Debug + shell: cmd + run: | + nuget restore .\TerrariaServerAPI\TShock.4.OTAPI.sln + "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TShock.4.OTAPI.sln /p:Configuration=Debug + cd .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Debug + TShock.Modifications.Bootstrapper.exe + - name: OTAPI Release + shell: cmd + run: | + nuget restore .\TerrariaServerAPI\TShock.4.OTAPI.sln + "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TShock.4.OTAPI.sln /p:Configuration=Release + cd .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Release + TShock.Modifications.Bootstrapper.exe + - name: TerrariaServerAPI Debug + shell: cmd + run: | + cd .\TerrariaServerAPI + "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TerrariaServerAPI.csproj /p:Configuration=Debug + - name: TShock Debug + shell: cmd + run: | + nuget restore TShock.sln + "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TShockAPI\TShockAPI.csproj /p:Configuration=Debug + - name: TerrariaServerAPI Release + shell: cmd + run: | + cd .\TerrariaServerAPI + "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TerrariaServerAPI\TerrariaServerAPI.csproj /p:Configuration=Release + - name: TShock Release + shell: cmd + run: | + nuget restore TShock.sln + "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" .\TShockAPI\TShockAPI.csproj /p:Configuration=Release + - uses: actions/upload-artifact@master + with: + name: Experimental TShock (not debug) + path: TShockAPI\bin\Release + - uses: actions/upload-artifact@master + with: + name: Experimental TShock (debug) + path: TShockAPI\bin\Debug + - uses: actions/upload-artifact@master + with: + name: Experimental (debug) OTAPI Bootstrapper + path: .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Debug\TShock.Modifications.Bootstrapper.exe + - uses: actions/upload-artifact@master + with: + name: Experimental (not debug) OTAPI Bootstrapper + path: .\TerrariaServerAPI\TShock.Modifications.Bootstrapper\bin\Release\TShock.Modifications.Bootstrapper.exe diff --git a/.gitignore b/.gitignore index c7755a6d..5ac4b6aa 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ Thumbs.db *.csproj.user */_ReSharper*/* *.user +.vs/* #Template Bat file# ################### diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5e74512c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: csharp -solution: "./TShockAPI/TShockAPI.csproj" -sudo: false -install: -- nuget restore -script: python ./scripts/create_release.py -notifications: - slack: - secure: O4Nibe2fdaUa2ZxuETUg6WEoQKvNM2CotnfaIVgm3fjfe61dfE1P+EgTpbwDG8646jSmpTqMDw8Z6I/WJwGTlXV/ZQsbwu63Cps4MgOTvPHZ0Lsye5azySlJZs1iI4ItYSj2czXfcnJ+qAl1SOOkXJrjB5uyTMWtDpCrSCFB3MA= - webhooks: - secure: dbTvcMtts5hSgV3DvlHPh36LTOvSPzQbVRUrgN9j0M/MlCm1QlBVt1vDLzN8VbkSYXiJYVWGMDpSHApL6SBu7sEQaXeC4zZyTMX76PeKw5a5xh0mIdDyg8Ls9WVA+QDVGes5DA1CZWbVRBDto3U0c+Ob8iza3o01sEFWpm7wQg4= \ No newline at end of file diff --git a/README.md b/README.md index 9fa0710d..dfa0672c 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,269 @@

TShock for Terraria
- Build StatusAppVeyor Build Status
-


+ + AppVeyor Build Status + + + GitHub Actions Build Status + + + All contributors + +

+ 查看中文版

-TShock is a server modification for Terraria, written in C#, and based upon the [Terraria Server API](https://github.com/NyxStudios/TerrariaAPI-Server). It uses JSON for configuration management, and offers several features not present in the Terraria Server normally. +TShock is a toolbox for Terraria servers and communities. That toolbox is jam packed with anti-cheat tools, server-side characters, groups, permissions, item bans, tons of commands, and limitless potential. It's one of a kind. -## :star: Quick Start +**We are currently updating for Terraria 1.4.0.1 (curRelease = 225). For updates, check our [twitter, @Pryaxis](https://twitter.com/Pryaxis).** -https://tshock.readme.io/docs/getting-started +* Download: [Stable](https://github.com/TShock/TShock/releases) or [Experimental](#experimental-downloads). +* Read [the documentation](https://tshock.readme.io/) to quickly get up to speed. +* Join [Discord](https://discord.gg/Cav9nYX). +* Join [Telegram](https://t.me/pryaxis). +* Download [other plugins](https://tshock.co/xf/index.php?resources/) to supercharge your server. -## Features +---- -* MySQL support -* Permissions -* Multiple administrators -* Anti-cheat -* User registration -* Reserved slots -* User punishment (kicking, banning, muting) -* Server side characters -* JSON based configuration management +## Table of Contents -## Community + * [New to TShock?](#new-to-tshock) + * [Experimental Downloads](#experimental-downloads) + * [Developer's Guide](#developers-guide) + * [Background](#background) + * [Building](#building) + * [On Windows](#on-windows) + * [The Terraria Server API](#the-terraria-server-api) + * [TShock](#tshock) + * [On macOS](#on-macos) + * [On Linux](#on-linux) + * [On Unix](#on-unix) + * [The Terraria Server API](#the-terraria-server-api-1) + * [TShock](#tshock-1) + * [Working with Terraria](#working-with-terraria) + * [Code of Conduct](#code-of-conduct) -Feeling like helping out? Want to find an awesome server? Some awesome plugins? +## New to TShock? -* [Website & Forums](https://tshock.co/xf/) -* [Contribute to our docs on readme.io](https://tshock.readme.io/) -* [Join our Discord chat (supports Android, iOS, Web, Mac, and Windows)](https://discord.gg/XUJdH58) +_These instructions assume Windows. If you're setting up on Linux or macOS, please refer to [the in-depth guide](https://tshock.readme.io/docs/getting-started) (and don't forget to install the *latest version* of `mono-complete` on Linux)._ -### Code of Conduct +1. Download [the latest stable version](https://github.com/TShock/TShock/releases) and `unzip` the folder using your favorite unzip tool. Make sure that all of the files in the zip get into one folder. This is where your server will be stored. The file structure looks like this: + + + GeoIP.dat + Newtonsoft.Json.dll + OTAPI.dll + ServerPlugins\ + |------BCrypt.Net.dll + |------HttpServer.dll + |------Mono.Data.Sqlite.dll + |------MySql.Data.dll + |------TShockAPI.dll + TerrariaServer.exe + sqlite3.dll + + +1. Start `TerrariaServer.exe` and TShock will boot. Answer the startup questions, and you should be ready to roll. In the background, TShock made some folders for you. We'll come back to those later. + +1. Startup Terraria. Connect to a `multiplayer` server via IP and enter `localhost` if you're doing this on your local computer. If you're doing it on another computer, you need its IP address. + +1. Look at the server console for the _setup code_. Type `/setup [code]` (example: `/setup 12345`), then a space, then the code you see in the console in your game chat. Instead of chatting, you'll run a command on the server. This one makes you temporary admin. All commands are prefixed with `/` or `!` (to make them silent). + +1. Use the in-game command `/user add [account name] [password] owner` (example: `/user add shank lovely-ashes owner`) to create an account. This gives you owner rights on your server, which you can configure more to your liking later. + +1. Login to your newly created account with `/login [account name] [password]` (example: `/login shank lovely-ashes`). You should see a login success message. + +1. Turn off the setup system with `/setup` and your server is setup for initial use. TShock also created several files inside a new `tshock` folder. These files include `config.json` (our big configuration file), `sscconfig.json` (the server side characters configuration file), and `tshock.sqlite`. Don't lose your `tshock.sqlite` or you'll have to re-setup TShock. + +1. You can now [customize your configuration](https://tshock.readme.io/docs/config-settings), build groups, ban items, and install more plugins. + +## Experimental Downloads + +To download experimental versions of TShock, you have two real options: AppVeyor builds or GitHub builds. You can also get archived Travis CI builds. Fair warning though: experimental versions of TShock are point-in-time releases that are not technically supported by us. If you have to report an issue, please make it clear which commit or branch you downloaded your build from, which service, and the build number if applicable. + +On [AppVeyor](https://ci.appveyor.com/project/hakusaro/tshock/), click on history, find the build you want, click on the commit message, and then click on the artifacts tab. You can download either the debug or the release build. AppVeyor only keeps builds back 6 months though. + +On [GitHub](https://github.com/Pryaxis/TShock/), click on the actions tab, then click on "build server" on the commit or branch you want. If it was successful, you can download either the experimental release or debug artifacts. + +For old builds from Travis CI, you can still get them (for now) from us directly, on [our Travis CI artifact mirror](https://travis.tshock.co/). Please note that these builds should be considered legacy and for archival purposes only. If you need them in the long term, please raise an issue explaining why before they are removed. + +## Developer's Guide + +Whether you want to contribute to TShock by sending a pull request, customize it to suit your own elvish desires, or want to build your own plugin, this is the best starting point. By the end of this, you'll be able to build TShock from source, start to finish. More than that, though, you'll know how to start on the path of becoming an expert TShock developer. + +But first, you need some background. + +### Background + +Terraria is a C# application written on the .NET framework using the XNA game framework. TShock is a mod for Terraria's server, which is also written in C# on the .NET framework. Some might compare TShock to hMod in the Minecraft world (the precursor to Bukkit and its server, CraftBukkit). This is a good comparison to make in how the underlying build process works. When the project started, TShock was injected directly into the decompiled source code for Terraria. Unlike Minecraft, Terraria is not obfuscated, which means that many variable names and inner workings are sanely-named out of the box. Now, TShock uses advanced techniques to operate. + +TShock is, first and foremost, a plugin written for the server variant of the Terraria API, an unofficial construct originally built by `bladecoding`. `TShock` has been colloquially used to refer to both the plugin as well as the server and plugin together. Similarly, the Terraria API's client version was abandoned long ago, and development of the `Server` API led to the abbreviation `TSAPI`, for `Terraria Server API`. The plugin `TShock` is executed by the [Terraria Server API](https://github.com/Pryaxis/TerrariaAPI-Server), which is in turn bound to the `Open Terraria API`, more commonly `OTAPI`. The [Open Terraria API](https://github.com/DeathCradle/Open-Terraria-API) is maintained by [DeathCradle](https://github.com/DeathCradle). + +Now, the way that `TShock` runs on `TSAPI` through `OTAPI` can be summarized as the following: + +1. The Open Terraria API deeply integrates with Terraria by modifying the official server's binary directly. This is done through rewriting the Terraria bytecode, the [CIL code](https://en.wikipedia.org/wiki/Common_Intermediate_Language), using a patching tool designed by DeathCradle and tools from the Mono project. For `TSAPI`, additional modifications are done to support TSAPI specific features. This done through the `TShock Mintaka Patcher`. +2. The `Terraria Server API` uses hooks provided by `OTAPI` to provide higher level hooks as well as legacy hooks for existing TSAPI applications. +3. `TShock` is executed by `TSAPI`, uses hooks provided by both `TSAPI` and `OTAPI`, and provides even higher level hooks and support tools to other `TSAPI` plugins. + +With all of this in mind, the primary goal when compiling TShock is to remember that only the second and third layers are required to be interacted with. The first layer, `OTAPI`, is provided pre-compiled through NuGet. The second layer, `TSAPI`, is provided in the `TShock` repository through a git submodule. Its primary home is the [Terraria Server API repository](https://github.com/Pryaxis/TerrariaAPI-Server). + +Let's get started. + +### Building + +You need to get the source code. Using git, [clone this repository](https://help.github.com/articles/cloning-a-repository/). + +The next set of instructions are the technical details to setup both the Terraria Server API and TShock. More importantly, the Terraria API steps here are written under the assumption that you are building TShock primarily. Before you start, you need to **initialize the git submodules** and then **update them**. You need to use the following commands to do this. + + $ git submodule init + $ git submodule update + +If you're using [GitHub Desktop](https://desktop.github.com), you need to perform additional steps. After cloning the TShock repository, go to the `Repository` menu and select `Open in Command Prompt`. If you don't have Git (not GitHub Desktop) installed, you can follow the prompts to to install Git for your command line. Once Git is installed, use this same process to get to the command prompt. Then, run the above commands. + +#### On Windows + +On Windows, you need to install [Visual Studio Community Edition](https://www.visualstudio.com/downloads/) or a better (more expensive) version of Visual Studio. + +##### The Terraria Server API + +1. Open the `TShock.4.OTAPI.sln` solution in the `TerrariaServerAPI` folder. + +1. Set the `TShock.Modifications.Bootstrapper` project as the StartUp project. + +1. Build the solution in either debug or release mode, depending on your preference. NuGet will automatically fetch the appropriate packages as a result of its magical powers. + +1. Hit the "Start" button in Visual Studio to run the `TShock Mintaka Bootstrapper`. + +1. Watch the output window and make sure that a non-zero number of modifications ran. When it completes, you have successfully bootstrapped `TShock Mintaka`. + +1. Set the `TerrariaServerAPI` project as the StartUp project. + +1. Build the solution in either debug or release mode, depending on your preference. + +1. Close `TShock.4.OTAPI.sln` in Visual Studio. + +You need to re-run the patcher any time `OTAPI` updates. You need to rebuild `TerrariaServerAPI` any time that the submodule in `TShock` gets changed, if you're doing this from inside the TShock repo. You also need to update the submodules (`git submodule update`) if they're out of date on a pull too. + +##### TShock + +1. Open the `TShock.sln` solution in the root of the repository. + +1. Build the solution. It should correctly download NuGet packages automatically and build against the aforementioned `TerrariaServerAPI` project you just built. + +#### On macOS + +1. Install [Homebrew](https://brew.sh) if you haven't already. + +1. Install mono: + + $ brew install mono + +1. Verify that mono is available: + + $ mono --version + + Mono JIT compiler version 5.0.1.1 (2017-02/5077205 Sun Sep 17 18:29:46 BST 2017) + ... + +1. Proceed to the [unix build steps](#unix-build-steps) to continue. + +#### On Linux + +1. **DO NOT** just install mono from your package manager unless told to do so. If you do and it's out of date, you probably won't be able to successfully develop for TShock. + +1. Follow the [official install instructions for mono](http://www.mono-project.com/download/). **DO** install `mono-complete` or you're missing components. + +1. Proceed to the [unix build steps](#unix-build-steps) to continue. + +#### On Unix + +1. You need to get NuGet. Download the latest `nuget.exe` from [NuGet](https://www.nuget.org/downloads). + +1. Make a `~/bin` folder if you don't have one. Then, put `nuget.exe` inside it. + + $ mkdir ~/bin/ + $ cp ~/downloads/nuget.exe ~/bin/ + +1. Set an environment variable to store if you plan to build in debug or release. + + $ export BUILD_MODE=Debug + + or + + $ export BUILD_MODE=Release + + +##### The Terraria Server API + +1. Perform a NuGet restore in the directory above `TerrariaServerAPI`. + + $ mono ~/bin/nuget.exe restore ./TerrariaServerAPI/ + +1. Build the `TShock.4.OTAPI.sln` solution the configuration you chose: + + $ xbuild ./TerrariaServerAPI/TShock.4.OTAPI.sln /p:Configuration=$BUILD_MODE + +1. Run the `TShock Mintaka Bootstrapper` with the TShock modifications. If you don't use `/bin/bash` as your primary shell, you might want to temporarily switch to it, or the bootstrapper may fail. + + $ cd ./TerrariaServerAPI/TShock.Modifications.Bootstrapper/bin/$BUILD_MODE/ + $ mono TShock.Modifications.Bootstrapper.exe -in=OTAPI.dll \ + -mod=../../../TShock.Modifications.**/bin/$BUILD_MODE/TShock.Modifications.*.dll \ + -o=Output/OTAPI.dll + +1. Verify that non-zero modifications ran successfully. Then, build the Terraria Server API executable. + + $ cd ./../../../ + $ xbuild ./TerrariaServerAPI/TerrariaServerAPI/TerrariaServerAPI.csproj \ + /p:Configuration=$BUILD_MODE + +You need to re-run the patcher any time `OTAPI` updates. You need to rebuild `TerrariaServerAPI` any time that the submodule in `TShock` gets changed, if you're doing this from inside the TShock repo. You also need to update the submodules (`git submodule update`) if they're out of date on a pull too. + +##### TShock + +1. Perform a NuGet restore in `TShockAPI` folder that contains `TShockAPI.sln`. + + $ mono ~/bin/nuget.exe restore + +1. Build TShock in the `BUILD_MODE` you set earlier. + + $ xbuild ./TShockAPI.sln /p:Configuration=$BUILD_MODE + +You're done! + +### Working with Terraria + +Working with Terraria in TShock and in other Terraria Server API plugins is different from most other APIs. Due to the nature of how OTAPI works, you have direct access to all public fields in the `Terraria` namespace. This means that you can access Terraria member methods directly. TShock and other plugins do this quite often, mostly to modify the game world, send data, and receive data. Calls to `Main` are one such example of direct access to Terraria. This is the equivalent to `net.minecraft.server` (NMS) calls in CraftBukkit. + +You might find yourself wondering where these fields are. Pryaxis provides the decompiled [Sources](https://github.com/pryaxis/Sources) to Terraria's server, updated with each release. Note that these decompiled servers do not re-compile. The process of fixing the decompiles has proven to be nearly impossible in a reasonable timeframe with the modern Terraria Server. + +Finally, you may be interested in developing other Terraria Server API plugins. The [TShockResources](https://github.com/TShockResources) organization has several plugins you can look at and build on. TShock is itself a plugin, and most plugins are open source. This gives you ample room to figure out where to go next. + +Need help? Join us on [Telegram](https://t.me/pryaxis) or [Discord](https://discord.gg/Cav9nYX). + +## Code of Conduct > By participating in the TShock for Terraria community, all members will adhere to maintaining decorum with respect to all humans, in and out of the community. Members will not engage in discussion that inappropriately disparages or marginalizes any group of people or any individual. Members will not attempt to further or advance an agenda to the point of being overbearing or close minded (such as through spreading FUD). Members will not abuse services provided to them and will follow the guidance of community leaders on a situational basis about what abuse consists of. Members will adhere to United States and international law. If members notice a violation of this code of conduct, they will not engage but will instead contact the leadership team on either the forums or Discord. > Do not attempt to circumvent or bypass the code of conduct by using clever logic or reasoning (e.g., insulting Facepunch members, because they weren't directly mentioned here). -Please see the contributing file before sending pull requests. +## Contributors -## Download +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): -* [Github Releases](https://github.com/TShock/TShock/releases) -* [Development Builds](https://travis.tshock.co/) -* [Plugins](https://tshock.co/xf/index.php?resources/) -* [Very, very old versions of TShock](https://github.com/TShock/TShock/downloads) + + + + + + + + + + +

AviKav

🐛 ⚠️

Rodrigo Rente

💻 📆 ⚠️

Stargazing Koishi

💻 🚇

Axeel

📖 📆
+ + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/README_cn.md b/README_cn.md new file mode 100644 index 00000000..ff900a06 --- /dev/null +++ b/README_cn.md @@ -0,0 +1,86 @@ +

+ TShock for Terraria
+ AppVeyor Build StatusGitHub Actions Build StatusAll contributors
+

+ +TShock是为泰拉瑞亚设计的多功能服务端。它拥有反作弊/强制开荒/用户组/权限管理/物品封禁/大量指令和无限的扩展性。 + +* 下载: [稳定版](https://github.com/TShock/TShock/releases) or [测试版](#experimental-downloads) +* 使用方法请阅读 [文档](https://tshock.readme.io/) +* 你可以加入 [我们的官方QQ群](https://jq.qq.com/?_wv=1027&k=5GJZCe4) 交流 +* 也可以加入 [我们的Discord服务器](https://discord.gg/Cav9nYX) 提问 +* 如果想要深度技术支持,可以加入 [我们的Telegram群](https://t.me/pryaxis) +* 你可以在 [这里](https://tshock.co/xf/index.php?resources/) 下载插件增强你的服务器 + +---- + +## 内容索引 + + * [第一次使用TShock?](#new-to-tshock) + * [下载测试版](#experimental-downloads) + +## 第一次使用TShock? + +_这篇指南基于Windows。如果你在使用Unix或者Linux,请参考 [深度指南](https://tshock.readme.io/docs/getting-started) (不要忘记在你的Linux系统上安装 **最新版** 的 `mono-complete` )._ + +1. 下载 [最新稳定版](https://github.com/TShock/TShock/releases) 然后解压。解压后文件所在的文件夹就是你服务器的工作目录。文件夹结构大致如下: + + + GeoIP.dat + Newtonsoft.Json.dll + OTAPI.dll + ServerPlugins\ + |------BCrypt.Net.dll + |------HttpServer.dll + |------Mono.Data.Sqlite.dll + |------MySql.Data.dll + |------TShockAPI.dll + TerrariaServer.exe + sqlite3.dll + + +1. 运行 `TerrariaServer.exe` ,TShock就会启动了。 TShock会自动创建一些文件夹,具体用途稍后讨论。 + +1. 启动你的游戏,选择 `多人模式` 并选择 `通过IP加入`。输入 `localhost` 或者 `127.0.0.1` 如果你的服务器和游戏运行在同一台电脑上。如果你在用其他设备开服,你需要输入它的IP地址。 + +1. 查看服务器控制台上的 _验证码_。在游戏里打开聊天窗口输入 `/setup [验证码]` (举个例子: `/setup 12345`)然后回车。这条指令可以让你成为临时管理。 所有指令都需要以 `/` 或者 `!` 开头。 + +1. 在游戏里输入指令 `/user add [账号名] [密码] owner` (举个例子: `/user add 鱼鱼 真可爱 owner`) 来创建一个账号并且给这个账号服主权限。 + +1. 登录你刚刚创建的账号,方法是输入指令 `/login [账号名] [密码]` (举个例子: `/login 鱼鱼 真可爱`) 然后你就会看到登录成功的提示。 + +1. 输入指令 `/setup` 关闭初始化设置功能,因为你已经搞定了。TShock会在 `tshock` 文件夹内创建数个文件。包括 `config.json` (服务器配置文件), `sscconfig.json` (强制开荒配置文件) 和 `tshock.sqlite` (服务器数据库)。不要把 `tshock.sqlite` 搞丢了,不然就白折腾了。 + +1. 现在你可以 [调整配置](https://tshock.readme.io/docs/config-settings) ,创建用户组,封禁物品或者安装插件了。 + +## 下载测试版 + +想下载测试版的TShock,你有两个选择:AppVeyor或者GitHub。你也可以获取Travis CI上的旧版本。注意: 测试版的TShock理论上不受我们的支持。如果你遇到问题需要发Issue,请提前声明你的版本信息。 + +在 [AppVeyor](https://ci.appveyor.com/project/hakusaro/tshock/) 上,点击History,找到需要的版本并点击, 然后点击Artifacts就可以下载它的发布版或者调试版。AppVeyor只会保留半年内的版本。 + +在 [GitHub项目](https://github.com/Pryaxis/TShock/) 页面里,点击 `Actions`,然后点击你想要的branch的 `build server` 就可以下载它的发布版或者调试版。 + +关于Travis CI上的旧版本,现在还可以在 [我们的Travis CI产物镜像](https://travis.tshock.co/) 上获取。但是请注意这些旧版本已经不再受支持。 + +## Contributors + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + +

AviKav

🐛 ⚠️

Rodrigo Rente

💻 📆 ⚠️

Stargazing Koishi

💻 🚇

Axeel

📖 📆
+ + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/TShockAPI/BackupManager.cs b/TShockAPI/BackupManager.cs index 752ad7ac..f624c746 100644 --- a/TShockAPI/BackupManager.cs +++ b/TShockAPI/BackupManager.cs @@ -1,6 +1,6 @@ /* TShock, a server mod for Terraria -Copyright (C) 2011-2017 Nyx Studios (fka. The TShock Team) +Copyright (C) 2011-2019 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 diff --git a/TShockAPI/Bouncer.cs b/TShockAPI/Bouncer.cs new file mode 100644 index 00000000..f55c7aaa --- /dev/null +++ b/TShockAPI/Bouncer.cs @@ -0,0 +1,1933 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2011-2019 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; + +namespace TShockAPI +{ + /// Bouncer is the TShock anti-hack and anti-cheat system. + internal sealed class Bouncer + { + /// Constructor call initializes Bouncer and related functionality. + /// A new Bouncer. + internal Bouncer() + { + // Setup hooks + GetDataHandlers.GetSection += OnGetSection; + GetDataHandlers.PlayerUpdate += OnPlayerUpdate; + GetDataHandlers.TileEdit += OnTileEdit; + GetDataHandlers.SendTileSquare += OnSendTileSquare; + GetDataHandlers.ItemDrop += OnItemDrop; + GetDataHandlers.NewProjectile += OnNewProjectile; + GetDataHandlers.NPCStrike += OnNPCStrike; + GetDataHandlers.ProjectileKill += OnProjectileKill; + GetDataHandlers.ChestItemChange += OnChestItemChange; + GetDataHandlers.ChestOpen += OnChestOpen; + GetDataHandlers.PlaceChest += OnPlaceChest; + GetDataHandlers.PlayerZone += OnPlayerZone; + GetDataHandlers.PlayerAnimation += OnPlayerAnimation; + GetDataHandlers.LiquidSet += OnLiquidSet; + GetDataHandlers.PlayerBuff += OnPlayerBuff; + GetDataHandlers.NPCAddBuff += OnNPCAddBuff; + GetDataHandlers.NPCHome += OnUpdateNPCHome; + GetDataHandlers.HealOtherPlayer += OnHealOtherPlayer; + GetDataHandlers.PlaceObject += OnPlaceObject; + GetDataHandlers.PlaceTileEntity += OnPlaceTileEntity; + GetDataHandlers.PlaceItemFrame += OnPlaceItemFrame; + GetDataHandlers.PortalTeleport += OnPlayerPortalTeleport; + GetDataHandlers.GemLockToggle += OnGemLockToggle; + GetDataHandlers.MassWireOperation += OnMassWireOperation; + GetDataHandlers.PlayerDamage += OnPlayerDamage; + GetDataHandlers.KillMe += OnKillMe; + } + + 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)) + { + args.Player.Kick("Your client sent a blank character name.", true, true); + args.Handled = true; + return; + } + + if (!args.Player.HasPermission(Permissions.ignorestackhackdetection)) + { + args.Player.IsDisabledForStackDetection = args.Player.HasHackedItemStacks(shouldWarnPlayer: true); + } + } + + /// Handles disabling enforcement and minor anti-exploit stuff + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnPlayerUpdate(object sender, GetDataHandlers.PlayerUpdateEventArgs args) + { + byte plr = args.PlayerId; + BitsByte control = args.Control; + BitsByte pulley = args.Pulley; + byte item = args.Item; + var pos = args.Position; + var vel = args.Velocity; + + if (pos.X < 0 || pos.Y < 0 || pos.X >= Main.maxTilesX * 16 - 16 || pos.Y >= Main.maxTilesY * 16 - 16) + { + args.Handled = true; + return; + } + + if (item < 0 || item >= args.Player.TPlayer.inventory.Length) + { + args.Handled = true; + return; + } + + if (args.Player.LastNetPosition == Vector2.Zero) + { + args.Handled = true; + return; + } + + if (!pos.Equals(args.Player.LastNetPosition)) + { + float distance = Vector2.Distance(new Vector2(pos.X / 16f, pos.Y / 16f), + new Vector2(args.Player.LastNetPosition.X / 16f, args.Player.LastNetPosition.Y / 16f)); + + if (args.Player.IsBeingDisabled()) + { + // If the player has moved outside the disabled zone... + if (distance > TShock.Config.MaxRangeForDisabled) + { + // We need to tell them they were disabled and why, then revert the change. + if (args.Player.IsDisabledForStackDetection) + { + args.Player.SendErrorMessage("Disabled. You went too far with hacked item stacks."); + } + else if (args.Player.IsDisabledForBannedWearable) + { + args.Player.SendErrorMessage("Disabled. You went too far with banned armor."); + } + else if (args.Player.IsDisabledForSSC) + { + args.Player.SendErrorMessage("Disabled. You need to {0}login to load your saved data.", TShock.Config.CommandSpecifier); + } + else if (TShock.Config.RequireLogin && !args.Player.IsLoggedIn) + { + args.Player.SendErrorMessage("Account needed! Please {0}register or {0}login to play!", TShock.Config.CommandSpecifier); + } + else if (args.Player.IsDisabledPendingTrashRemoval) + { + args.Player.SendErrorMessage("You need to rejoin to ensure your trash can is cleared!"); + } + + // ?? + var lastTileX = args.Player.LastNetPosition.X; + var lastTileY = args.Player.LastNetPosition.Y - 48; + if (!args.Player.Teleport(lastTileX, lastTileY)) + { + args.Player.Spawn(); + } + args.Handled = true; + return; + } + args.Handled = true; + return; + } + + // Corpses don't move + if (args.Player.Dead) + { + args.Handled = true; + return; + } + } + + return; + } + + /// Bouncer's TileEdit hook is used to revert malicious tile changes. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnTileEdit(object sender, GetDataHandlers.TileEditEventArgs args) + { + EditAction action = args.Action; + int tileX = args.X; + int tileY = args.Y; + short editData = args.EditData; + EditType type = args.editDetail; + byte style = args.Style; + + try + { + if (editData < 0) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + if (!TShock.Utils.TilePlacementValid(tileX, tileY)) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (action == EditAction.KillTile && Main.tile[tileX, tileY].type == TileID.MagicalIceBlock) + { + args.Handled = false; + return; + } + + if (args.Player.Dead && TShock.Config.PreventDeadModification) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + Item selectedItem = args.Player.SelectedItem; + int lastKilledProj = args.Player.LastKilledProjectile; + ITile tile = Main.tile[tileX, tileY]; + + if (action == EditAction.PlaceTile) + { + if (TShock.TileBans.TileIsBanned(editData, args.Player)) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Player.SendErrorMessage("You do not have permission to place this tile."); + args.Handled = true; + return; + } + } + + if (action == EditAction.KillTile && !Main.tileCut[tile.type] && !breakableTiles.Contains(tile.type)) + { + //TPlayer.mount.Type 8 => Drill Containment Unit. + + // If the tile is an axe tile and they aren't selecting an axe, they're hacking. + if (Main.tileAxe[tile.type] && ((args.Player.TPlayer.mount.Type != 8 && selectedItem.axe == 0) && !ItemID.Sets.Explosives[selectedItem.netID] && args.Player.RecentFuse == 0)) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + // If the tile is a hammer tile and they aren't selecting a hammer, they're hacking. + else if (Main.tileHammer[tile.type] && ((args.Player.TPlayer.mount.Type != 8 && selectedItem.hammer == 0) && !ItemID.Sets.Explosives[selectedItem.netID] && args.Player.RecentFuse == 0)) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + // If the tile is a pickaxe tile and they aren't selecting a pickaxe, they're hacking. + // Item frames can be modified without pickaxe tile. + else if (tile.type != TileID.ItemFrame + && !Main.tileAxe[tile.type] && !Main.tileHammer[tile.type] && tile.wall == 0 && args.Player.TPlayer.mount.Type != 8 && selectedItem.pick == 0 && !ItemID.Sets.Explosives[selectedItem.netID] && args.Player.RecentFuse == 0) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + } + else if (action == EditAction.KillWall) + { + // If they aren't selecting a hammer, they could be hacking. + if (selectedItem.hammer == 0 && !ItemID.Sets.Explosives[selectedItem.netID] && args.Player.RecentFuse == 0 && selectedItem.createWall == 0) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + } + else if (action == EditAction.PlaceTile && (projectileCreatesTile.ContainsKey(lastKilledProj) && editData == projectileCreatesTile[lastKilledProj])) + { + args.Player.LastKilledProjectile = 0; + } + else if (action == EditAction.PlaceTile || action == EditAction.PlaceWall) + { + if ((action == EditAction.PlaceTile && TShock.Config.PreventInvalidPlaceStyle) && + (MaxPlaceStyles.ContainsKey(editData) && style > MaxPlaceStyles[editData]) && + (ExtraneousPlaceStyles.ContainsKey(editData) && style > ExtraneousPlaceStyles[editData])) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + // If they aren't selecting the item which creates the tile or wall, they're hacking. + if (!(selectedItem.netID == ItemID.IceRod && editData == TileID.MagicalIceBlock) && + (editData != (action == EditAction.PlaceTile ? selectedItem.createTile : selectedItem.createWall) && + !(ropeCoilPlacements.ContainsKey(selectedItem.netID) && editData == ropeCoilPlacements[selectedItem.netID]))) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + // Using the actuation accessory can lead to actuator hacking + if (TShock.Itembans.ItemIsBanned("Actuator", args.Player) && args.Player.TPlayer.autoActuator) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Player.SendErrorMessage("You do not have permission to place actuators."); + args.Handled = true; + return; + } + if (TShock.Itembans.ItemIsBanned(EnglishLanguage.GetItemNameById(selectedItem.netID), args.Player) || editData >= (action == EditAction.PlaceTile ? Main.maxTileSets : Main.maxWallTypes)) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + if (action == EditAction.PlaceTile && (editData == TileID.PiggyBank || editData == TileID.Safes) && Main.ServerSideCharacter) + { + args.Player.SendErrorMessage("You cannot place this tile because server side characters are enabled."); + args.Player.SendTileSquare(tileX, tileY, 3); + args.Handled = true; + return; + } + if (action == EditAction.PlaceTile && (editData == TileID.Containers || editData == TileID.Containers2)) + { + if (TShock.Utils.HasWorldReachedMaxChests()) + { + args.Player.SendErrorMessage("The world's chest limit has been reached - unable to place more."); + args.Player.SendTileSquare(tileX, tileY, 3); + args.Handled = true; + return; + } + if ((TShock.Utils.TilePlacementValid(tileX, tileY + 1) && Main.tile[tileX, tileY + 1].type == TileID.Boulder) || + (TShock.Utils.TilePlacementValid(tileX + 1, tileY + 1) && Main.tile[tileX + 1, tileY + 1].type == TileID.Boulder)) + { + args.Player.SendTileSquare(tileX, tileY, 3); + args.Handled = true; + return; + } + } + } + else if (action == EditAction.PlaceWire || action == EditAction.PlaceWire2 || action == EditAction.PlaceWire3) + { + // If they aren't selecting a wrench, they're hacking. + // WireKite = The Grand Design + if (selectedItem.type != ItemID.Wrench + && selectedItem.type != ItemID.BlueWrench + && selectedItem.type != ItemID.GreenWrench + && selectedItem.type != ItemID.YellowWrench + && selectedItem.type != ItemID.MulticolorWrench + && selectedItem.type != ItemID.WireKite) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + } + else if (action == EditAction.KillActuator || action == EditAction.KillWire || + action == EditAction.KillWire2 || action == EditAction.KillWire3) + { + // If they aren't selecting the wire cutter, they're hacking. + if (selectedItem.type != ItemID.WireCutter + && selectedItem.type != ItemID.WireKite + && selectedItem.type != ItemID.MulticolorWrench) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + } + else if (action == EditAction.PlaceActuator) + { + // If they aren't selecting the actuator and don't have the Presserator equipped, they're hacking. + if (selectedItem.type != ItemID.Actuator && !args.Player.TPlayer.autoActuator) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + } + if (TShock.Config.AllowCutTilesAndBreakables && Main.tileCut[tile.type]) + { + if (action == EditAction.KillWall) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + args.Handled = false; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + if (!args.Player.HasModifiedIceSuccessfully(tileX, tileY, editData, action) + && !args.Player.HasBuildPermission(tileX, tileY)) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(tileX, tileY)) + { + if (action == EditAction.PlaceTile && (editData == TileID.Rope || editData == TileID.SilkRope || editData == TileID.VineRope || editData == TileID.WebRope)) + { + args.Handled = false; + return; + } + + if (action == EditAction.KillTile || action == EditAction.KillWall && ItemID.Sets.Explosives[selectedItem.netID] && args.Player.RecentFuse == 0) + { + args.Handled = false; + return; + } + + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + if (args.Player.TileKillThreshold >= TShock.Config.TileKillThreshold) + { + args.Player.Disable("Reached TileKill threshold.", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + if (args.Player.TilePlaceThreshold >= TShock.Config.TilePlaceThreshold) + { + args.Player.Disable("Reached TilePlace threshold.", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + if (args.Player.IsBouncerThrottled()) + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + + if ((action == EditAction.PlaceTile || action == EditAction.PlaceWall) && !args.Player.HasPermission(Permissions.ignoreplacetiledetection)) + { + args.Player.TilePlaceThreshold++; + var coords = new Vector2(tileX, tileY); + lock (args.Player.TilesCreated) + if (!args.Player.TilesCreated.ContainsKey(coords)) + args.Player.TilesCreated.Add(coords, Main.tile[tileX, tileY]); + } + + if ((action == EditAction.KillTile || action == EditAction.KillTileNoItem || action == EditAction.KillWall) && Main.tileSolid[Main.tile[tileX, tileY].type] && + !args.Player.HasPermission(Permissions.ignorekilltiledetection)) + { + args.Player.TileKillThreshold++; + var coords = new Vector2(tileX, tileY); + lock (args.Player.TilesDestroyed) + if (!args.Player.TilesDestroyed.ContainsKey(coords)) + args.Player.TilesDestroyed.Add(coords, Main.tile[tileX, tileY]); + } + args.Handled = false; + return; + } + catch + { + args.Player.SendTileSquare(tileX, tileY, 4); + args.Handled = true; + return; + } + } + + /// Bouncer's SendTileSquare hook halts large scope world destruction. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnSendTileSquare(object sender, GetDataHandlers.SendTileSquareEventArgs args) + { + short size = args.Size; + int tileX = args.TileX; + int tileY = args.TileY; + + if (args.Player.HasPermission(Permissions.allowclientsideworldedit)) + { + args.Handled = false; + return; + } + + // From White: + // IIRC it's because 5 means a 5x5 square which is normal for a tile square, and anything bigger is a non-vanilla tile modification attempt + if (size > 5) + { + args.Handled = true; + return; + } + + if (args.Player.IsBouncerThrottled()) + { + args.Player.SendTileSquare(tileX, tileY, size); + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendTileSquare(tileX, tileY, size); + args.Handled = true; + return; + } + + try + { + var tiles = new NetTile[size, size]; + for (int x = 0; x < size; x++) + { + for (int y = 0; y < size; y++) + { + tiles[x, y] = new NetTile(args.Data); + } + } + + bool changed = false; + for (int x = 0; x < size; x++) + { + int realx = tileX + x; + if (realx < 0 || realx >= Main.maxTilesX) + continue; + + for (int y = 0; y < size; y++) + { + int realy = tileY + y; + if (realy < 0 || realy >= Main.maxTilesY) + continue; + + var tile = Main.tile[realx, realy]; + var newtile = tiles[x, y]; + if (!args.Player.HasBuildPermission(realx, realy) || + !args.Player.IsInRange(realx, realy)) + { + continue; + } + + // Fixes the Flower Boots not creating flowers issue + if (size == 1 && args.Player.Accessories.Any(i => i.active && i.netID == ItemID.FlowerBoots)) + { + if (Main.tile[realx, realy + 1].type == TileID.Grass && (newtile.Type == TileID.Plants || newtile.Type == TileID.Plants2)) + { + args.Handled = false; + return; + } + + if (Main.tile[realx, realy + 1].type == TileID.HallowedGrass && (newtile.Type == TileID.HallowedPlants || newtile.Type == TileID.HallowedPlants2)) + { + args.Handled = false; + return; + } + + if (Main.tile[realx, realy + 1].type == TileID.JungleGrass && newtile.Type == TileID.JunglePlants2) + { + args.Handled = false; + return; + } + } + + // Junction Box + if (tile.type == TileID.WirePipe) + { + args.Handled = false; + return; + } + + // Orientable tiles + if (tile.type == newtile.Type && orientableTiles.Contains(tile.type)) + { + Main.tile[realx, realy].frameX = newtile.FrameX; + Main.tile[realx, realy].frameY = newtile.FrameY; + changed = true; + } + + // Landmine + if (tile.type == TileID.LandMine && !newtile.Active) + { + Main.tile[realx, realy].active(false); + changed = true; + } + + // Tile entities: sensors, item frames, training dummies + // here it handles all tile entities listed in `TileEntityID` + if ((newtile.Type == TileID.LogicSensor || + newtile.Type == TileID.ItemFrame || + newtile.Type == TileID.TargetDummy) && + !Main.tile[realx, realy].active()) + { + Main.tile[realx, realy].type = newtile.Type; + Main.tile[realx, realy].frameX = newtile.FrameX; + Main.tile[realx, realy].frameY = newtile.FrameY; + Main.tile[realx, realy].active(true); + changed = true; + } + + if (tile.active() && newtile.Active && tile.type != newtile.Type) + { + // Grass <-> Grass + if ((TileID.Sets.Conversion.Grass[tile.type] && TileID.Sets.Conversion.Grass[newtile.Type]) || + // Dirt <-> Dirt + ((tile.type == 0 || tile.type == 59) && + (newtile.Type == 0 || newtile.Type == 59)) || + // Ice <-> Ice + (TileID.Sets.Conversion.Ice[tile.type] && TileID.Sets.Conversion.Ice[newtile.Type]) || + // Stone <-> Stone + ((TileID.Sets.Conversion.Stone[tile.type] || Main.tileMoss[tile.type]) && + (TileID.Sets.Conversion.Stone[newtile.Type] || Main.tileMoss[newtile.Type])) || + // Sand <-> Sand + (TileID.Sets.Conversion.Sand[tile.type] && TileID.Sets.Conversion.Sand[newtile.Type]) || + // Sandstone <-> Sandstone + (TileID.Sets.Conversion.Sandstone[tile.type] && TileID.Sets.Conversion.Sandstone[newtile.Type]) || + // Hardened Sand <-> Hardened Sand + (TileID.Sets.Conversion.HardenedSand[tile.type] && TileID.Sets.Conversion.HardenedSand[newtile.Type])) + { + Main.tile[realx, realy].type = newtile.Type; + changed = true; + } + } + + // Stone wall <-> Stone wall + if (((tile.wall == 1 || tile.wall == 3 || tile.wall == 28 || tile.wall == 83) && + (newtile.Wall == 1 || newtile.Wall == 3 || newtile.Wall == 28 || newtile.Wall == 83)) || + // Leaf wall <-> Leaf wall + (((tile.wall >= 63 && tile.wall <= 70) || tile.wall == 81) && + ((newtile.Wall >= 63 && newtile.Wall <= 70) || newtile.Wall == 81))) + { + Main.tile[realx, realy].wall = newtile.Wall; + changed = true; + } + + if ((tile.type == TileID.TrapdoorClosed && (newtile.Type == TileID.TrapdoorOpen || !newtile.Active)) || + (tile.type == TileID.TrapdoorOpen && (newtile.Type == TileID.TrapdoorClosed || !newtile.Active)) || + (!tile.active() && newtile.Active && (newtile.Type == TileID.TrapdoorOpen || newtile.Type == TileID.TrapdoorClosed))) + { + Main.tile[realx, realy].type = newtile.Type; + Main.tile[realx, realy].frameX = newtile.FrameX; + Main.tile[realx, realy].frameY = newtile.FrameY; + Main.tile[realx, realy].active(newtile.Active); + changed = true; + } + } + } + + if (changed) + { + TSPlayer.All.SendTileSquare(tileX, tileY, size + 1); + WorldGen.RangeFrame(tileX, tileY, tileX + size, tileY + size); + } + else + { + args.Player.SendTileSquare(tileX, tileY, size); + } + } + catch + { + args.Player.SendTileSquare(tileX, tileY, size); + } + + args.Handled = true; + } + + /// Registered when items fall to the ground to prevent cheating. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnItemDrop(object sender, GetDataHandlers.ItemDropEventArgs 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; + + // player is attempting to crash clients + if (type < -48 || type >= Main.maxItemTypes) + { + // Causes item duplications. Will be re added later if necessary + //args.Player.SendData(PacketTypes.ItemDrop, "", id); + args.Handled = true; + return; + } + + // make sure the prefix is a legit value + // Note: Not checking if prefix is less than 1 because if it is, this check + // will break item pickups on the client. + if (prefix > PrefixID.Count) + { + args.Player.SendData(PacketTypes.ItemDrop, "", id); + args.Handled = true; + return; + } + + //Item removed, let client do this to prevent item duplication + // client side (but only if it passed the range check) (i.e., return false) + 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); + args.Handled = true; + return; + } + + args.Handled = false; + return; + } + + if (!args.Player.IsInRange((int)(pos.X / 16f), (int)(pos.Y / 16f))) + { + args.Player.SendData(PacketTypes.ItemDrop, "", id); + args.Handled = true; + return; + } + + // stop the client from changing the item type of a drop but + // only if the client isn't picking up the item + if (Main.item[id].active && Main.item[id].netID != type) + { + 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.ItemIsBanned(EnglishLanguage.GetItemNameById(item.type), args.Player) && !args.Player.HasPermission(Permissions.allowdroppingbanneditems))) + { + args.Player.SendData(PacketTypes.ItemDrop, "", 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.LogonDiscardThreshold)) + { + //Player is probably trying to sneak items onto the server in their hands!!! + TShock.Log.ConsoleInfo("Player {0} tried to sneak {1} onto the server!", args.Player.Name, item.Name); + args.Player.SendData(PacketTypes.ItemDrop, "", id); + args.Handled = true; + return; + + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendData(PacketTypes.ItemDrop, "", id); + args.Handled = true; + 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. + internal void OnNewProjectile(object sender, GetDataHandlers.NewProjectileEventArgs args) + { + short ident = args.Identity; + Vector2 pos = args.Position; + Vector2 vel = args.Velocity; + float knockback = args.Knockback; + short damage = args.Damage; + byte owner = args.Owner; + short type = args.Type; + int index = args.Index; + + if (index > Main.maxProjectiles) + { + args.Player.RemoveProjectile(ident, owner); + args.Handled = true; + return; + } + + if (TShock.ProjectileBans.ProjectileIsBanned(type, args.Player)) + { + args.Player.Disable(String.Format("Player does not have permission to create projectile {0}.", type), DisableFlags.WriteToLogAndConsole); + args.Player.SendErrorMessage("You do not have permission to create that projectile."); + args.Player.RemoveProjectile(ident, owner); + args.Handled = true; + return; + } + + if (damage > TShock.Config.MaxProjDamage && !args.Player.HasPermission(Permissions.ignoredamagecap)) + { + args.Player.Disable(String.Format("Projectile damage is higher than {0}.", TShock.Config.MaxProjDamage), DisableFlags.WriteToLogAndConsole); + args.Player.RemoveProjectile(ident, owner); + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.RemoveProjectile(ident, owner); + args.Handled = true; + return; + } + + // 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 + || type == ProjectileID.CultistBossIceMist + || (type >= ProjectileID.MoonlordBullet && type <= ProjectileID.MoonlordTurretLaser) + || type == ProjectileID.DeathLaser || type == ProjectileID.Landmine + || type == ProjectileID.BulletDeadeye || type == ProjectileID.BoulderStaffOfEarth + || (type > ProjectileID.ConfettiMelee && type < ProjectileID.SpiritHeal) + || (type >= ProjectileID.FlamingWood && type <= ProjectileID.GreekFire3) + || (type >= ProjectileID.PineNeedleHostile && type <= ProjectileID.Spike) + || (type >= ProjectileID.MartianTurretBolt && type <= ProjectileID.RayGunnerLaser) + || type == ProjectileID.CultistBossLightningOrb) + { + TShock.Log.Debug("Certain projectiles have been ignored for cheat detection."); + } + else + { + args.Player.Disable(String.Format("Does not have projectile permission to update projectile. ({0})", type), DisableFlags.WriteToLogAndConsole); + args.Player.RemoveProjectile(ident, owner); + } + args.Handled = true; + return; + } + + if (args.Player.ProjectileThreshold >= TShock.Config.ProjectileThreshold) + { + args.Player.Disable("Reached projectile update threshold.", DisableFlags.WriteToLogAndConsole); + args.Player.RemoveProjectile(ident, owner); + args.Handled = true; + return; + } + + if (args.Player.IsBouncerThrottled()) + { + args.Player.RemoveProjectile(ident, owner); + args.Handled = true; + return; + } + + if (!args.Player.HasPermission(Permissions.ignoreprojectiledetection)) + { + if (type == ProjectileID.CrystalShard && TShock.Config.ProjIgnoreShrapnel) // Ignore crystal shards + { + TShock.Log.Debug("Ignoring shrapnel per config.."); + } + else if (!Main.projectile[index].active) + { + args.Player.ProjectileThreshold++; // Creating new projectile + } + } + + if ((type == ProjectileID.Bomb + || type == ProjectileID.Dynamite + || type == ProjectileID.StickyBomb + || type == ProjectileID.StickyDynamite)) + { + // Denotes that the player has recently set a fuse - used for cheat detection. + args.Player.RecentFuse = 10; + } + } + + /// Handles the NPC Strike event for Bouncer. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnNPCStrike(object sender, GetDataHandlers.NPCStrikeEventArgs args) + { + short id = args.ID; + byte direction = args.Direction; + short damage = args.Damage; + float knockback = args.Knockback; + byte crit = args.Critical; + + if (Main.npc[id] == null) + { + args.Handled = true; + return; + } + + if (damage > TShock.Config.MaxDamage && !args.Player.HasPermission(Permissions.ignoredamagecap)) + { + if (TShock.Config.KickOnDamageThresholdBroken) + { + args.Player.Kick(string.Format("NPC damage exceeded {0}.", TShock.Config.MaxDamage)); + args.Handled = true; + return; + } + else + { + args.Player.Disable(String.Format("NPC damage exceeded {0}.", TShock.Config.MaxDamage), DisableFlags.WriteToLogAndConsole); + } + args.Player.SendData(PacketTypes.NpcUpdate, "", id); + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendData(PacketTypes.NpcUpdate, "", id); + args.Handled = true; + return; + } + + if (TShock.Config.RangeChecks && + !args.Player.IsInRange((int)(Main.npc[id].position.X / 16f), (int)(Main.npc[id].position.Y / 16f), 128)) + { + args.Player.SendData(PacketTypes.NpcUpdate, "", id); + args.Handled = true; + return; + } + + if (args.Player.IsBouncerThrottled()) + { + args.Player.SendData(PacketTypes.NpcUpdate, "", id); + args.Handled = true; + return; + } + } + + /// Handles ProjectileKill events for throttling and out of bounds projectiles. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnProjectileKill(object sender, GetDataHandlers.ProjectileKillEventArgs args) + { + if (args.ProjectileIndex < 0) + { + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.RemoveProjectile(args.ProjectileIdentity, args.ProjectileOwner); + args.Handled = true; + return; + } + + if (args.Player.IsBouncerThrottled()) + { + args.Player.RemoveProjectile(args.ProjectileIdentity, args.ProjectileOwner); + args.Handled = true; + return; + } + } + + /// Handles when a chest item is changed. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnChestItemChange(object sender, GetDataHandlers.ChestItemEventArgs args) + { + short id = args.ID; + byte slot = args.Slot; + short stacks = args.Stacks; + byte prefix = args.Prefix; + short type = args.Type; + + if (args.Player.TPlayer.chest != id) + { + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendData(PacketTypes.ChestItem, "", id, slot); + args.Handled = true; + return; + } + + if (!args.Player.HasBuildPermission(Main.chest[id].x, Main.chest[id].y) && TShock.Config.RegionProtectChests) + { + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(Main.chest[id].x, Main.chest[id].y)) + { + args.Handled = true; + return; + } + } + + /// The Bouncer handler for when chests are opened. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnChestOpen(object sender, GetDataHandlers.ChestOpenEventArgs args) + { + if (args.Player.IsBeingDisabled()) + { + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(args.X, args.Y)) + { + args.Handled = true; + return; + } + + if (!args.Player.HasBuildPermission(args.X, args.Y) && TShock.Config.RegionProtectChests) + { + args.Handled = true; + return; + } + + int id = Chest.FindChest(args.X, args.Y); + args.Player.ActiveChest = id; + } + + /// The place chest event that Bouncer hooks to prevent accidental damage. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnPlaceChest(object sender, GetDataHandlers.PlaceChestEventArgs args) + { + int tileX = args.TileX; + int tileY = args.TileY; + int flag = args.Flag; + + if (!TShock.Utils.TilePlacementValid(tileX, tileY) || (args.Player.Dead && TShock.Config.PreventDeadModification)) + { + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendTileSquare(tileX, tileY, 3); + args.Handled = true; + return; + } + + if (flag != 0 && flag != 4 // if no container or container2 placement + && Main.tile[tileX, tileY].type != TileID.Containers + && Main.tile[tileX, tileY].type != TileID.Dressers + && Main.tile[tileX, tileY].type != TileID.Containers2 + && (!TShock.Utils.HasWorldReachedMaxChests() && Main.tile[tileX, tileY].type != TileID.Dirt)) //Chest + { + args.Player.SendTileSquare(tileX, tileY, 3); + args.Handled = true; + return; + } + + if (flag == 2) //place dresser + { + if ((TShock.Utils.TilePlacementValid(tileX, tileY + 1) && Main.tile[tileX, tileY + 1].type == TileID.Teleporter) || + (TShock.Utils.TilePlacementValid(tileX + 1, tileY + 1) && Main.tile[tileX + 1, tileY + 1].type == TileID.Teleporter)) + { + //Prevent a dresser from being placed on a teleporter, as this can cause client and server crashes. + args.Player.SendTileSquare(tileX, tileY, 3); + args.Handled = true; + return; + } + } + + if (!args.Player.HasBuildPermission(tileX, tileY)) + { + args.Player.SendTileSquare(tileX, tileY, 3); + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(tileX, tileY)) + { + args.Player.SendTileSquare(tileX, tileY, 3); + args.Handled = true; + return; + } + } + + /// Handles PlayerZone events for preventing spawning NPC maliciously. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnPlayerZone(object sender, GetDataHandlers.PlayerZoneEventArgs args) + { + if (args.Zone2[1] || args.Zone2[2] || args.Zone2[3] || args.Zone2[4]) + { + bool hasSolarTower = false; + bool hasVortexTower = false; + bool hasNebulaTower = false; + bool hasStardustTower = false; + + foreach (var npc in Main.npc) + { + if (npc.netID == NPCID.LunarTowerSolar) + hasSolarTower = true; + else if (npc.netID == NPCID.LunarTowerVortex) + hasVortexTower = true; + else if (npc.netID == NPCID.LunarTowerNebula) + hasNebulaTower = true; + else if (npc.netID == NPCID.LunarTowerStardust) + hasStardustTower = true; + } + + if ((args.Zone2[1] && !hasSolarTower) + || (args.Zone2[2] && !hasVortexTower) + || (args.Zone2[3] && !hasNebulaTower) + || (args.Zone2[4] && !hasStardustTower) + ) + { + args.Handled = true; + return; + } + } + } + + /// Handles basic animation throttling for disabled players. + /// sender + /// args + internal void OnPlayerAnimation(object sender, GetDataHandlers.PlayerAnimationEventArgs args) + { + if (args.Player.IsBeingDisabled()) + { + args.Player.SendData(PacketTypes.PlayerAnimation, "", args.Player.Index); + args.Handled = true; + return; + } + + if (args.Player.IsBouncerThrottled()) + { + args.Player.SendData(PacketTypes.PlayerAnimation, "", args.Player.Index); + args.Handled = true; + return; + } + } + + /// Handles Bouncer's liquid set anti-cheat. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnLiquidSet(object sender, GetDataHandlers.LiquidSetEventArgs args) + { + int tileX = args.TileX; + int tileY = args.TileY; + byte amount = args.Amount; + byte type = args.Type; + + if (!TShock.Utils.TilePlacementValid(tileX, tileY) || (args.Player.Dead && TShock.Config.PreventDeadModification)) + { + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (args.Player.TileLiquidThreshold >= TShock.Config.TileLiquidThreshold) + { + args.Player.Disable("Reached TileLiquid threshold.", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (!args.Player.HasPermission(Permissions.ignoreliquidsetdetection)) + { + args.Player.TileLiquidThreshold++; + } + + // Liquid anti-cheat + // Arguably the banned buckets bit should be in the item bans system + if (amount != 0) + { + int bucket = -1; + if (args.Player.TPlayer.inventory[args.Player.TPlayer.selectedItem].type == ItemID.EmptyBucket) + { + bucket = 0; + } + else if (args.Player.TPlayer.inventory[args.Player.TPlayer.selectedItem].type == ItemID.WaterBucket) + { + bucket = 1; + } + else if (args.Player.TPlayer.inventory[args.Player.TPlayer.selectedItem].type == ItemID.LavaBucket) + { + bucket = 2; + } + else if (args.Player.TPlayer.inventory[args.Player.TPlayer.selectedItem].type == ItemID.HoneyBucket) + { + bucket = 3; + } + else if (args.Player.TPlayer.inventory[args.Player.TPlayer.selectedItem].type == ItemID.BottomlessBucket || + args.Player.TPlayer.inventory[args.Player.TPlayer.selectedItem].type == ItemID.SuperAbsorbantSponge) + { + bucket = 4; + } + + if (type == 1 && !(bucket == 2 || bucket == 0)) + { + args.Player.SendErrorMessage("You do not have permission to perform this action."); + args.Player.Disable("Spreading lava without holding a lava bucket", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (type == 1 && TShock.Itembans.ItemIsBanned("Lava Bucket", args.Player)) + { + args.Player.SendErrorMessage("You do not have permission to perform this action."); + args.Player.Disable("Using banned lava bucket without permissions", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (type == 0 && !(bucket == 1 || bucket == 0 || bucket == 4)) + { + args.Player.SendErrorMessage("You do not have permission to perform this action."); + args.Player.Disable("Spreading water without holding a water bucket", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (type == 0 && TShock.Itembans.ItemIsBanned("Water Bucket", args.Player)) + { + args.Player.SendErrorMessage("You do not have permission to perform this action."); + args.Player.Disable("Using banned water bucket without permissions", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (type == 2 && !(bucket == 3 || bucket == 0)) + { + args.Player.SendErrorMessage("You do not have permission to perform this action."); + args.Player.Disable("Spreading honey without holding a honey bucket", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (type == 2 && TShock.Itembans.ItemIsBanned("Honey Bucket", args.Player)) + { + args.Player.SendErrorMessage("You do not have permission to perform this action."); + args.Player.Disable("Using banned honey bucket without permissions", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + } + + if (!args.Player.HasBuildPermission(tileX, tileY)) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(tileX, tileY, 16)) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + + if (args.Player.IsBouncerThrottled()) + { + args.Player.SendTileSquare(tileX, tileY, 1); + args.Handled = true; + return; + } + } + + /// Handles Buff events. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnPlayerBuff(object sender, GetDataHandlers.PlayerBuffEventArgs args) + { + byte id = args.ID; + byte type = args.Type; + int time = args.Time; + + if (TShock.Players[id] == null) + { + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendData(PacketTypes.PlayerAddBuff, "", id); + args.Handled = true; + return; + } + + if (id >= Main.maxPlayers) + { + args.Player.SendData(PacketTypes.PlayerAddBuff, "", id); + args.Handled = true; + return; + } + + if (!TShock.Players[id].TPlayer.hostile || !Main.pvpBuff[type]) + { + args.Player.SendData(PacketTypes.PlayerAddBuff, "", id); + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(TShock.Players[id].TileX, TShock.Players[id].TileY, 50)) + { + args.Player.SendData(PacketTypes.PlayerAddBuff, "", id); + args.Handled = true; + return; + } + + if (args.Player.IsBouncerThrottled()) + { + args.Player.SendData(PacketTypes.PlayerAddBuff, "", id); + args.Handled = true; + return; + } + + if (WhitelistBuffMaxTime[type] > 0 && time <= WhitelistBuffMaxTime[type]) + { + args.Handled = false; + return; + } + } + + /// Handles NPCAddBuff events. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnNPCAddBuff(object sender, GetDataHandlers.NPCAddBuffEventArgs args) + { + short id = args.ID; + byte type = args.Type; + short time = args.Time; + + if (id >= Main.npc.Length) + { + args.Handled = true; + return; + } + + NPC npc = Main.npc[id]; + + if (npc == null) + { + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Handled = true; + return; + } + + bool detectedNPCBuffTimeCheat = false; + + if (NPCAddBuffTimeMax.ContainsKey(type)) + { + if (time > NPCAddBuffTimeMax[type]) + { + detectedNPCBuffTimeCheat = true; + } + + if (npc.townNPC && npc.netID != NPCID.Guide && npc.netID != NPCID.Clothier) + { + if (type != BuffID.Lovestruck && type != BuffID.Stinky && type != BuffID.DryadsWard && + type != BuffID.Wet && type != BuffID.Slimed) + { + detectedNPCBuffTimeCheat = true; + } + } + } + else + { + detectedNPCBuffTimeCheat = true; + } + + if (detectedNPCBuffTimeCheat) + { + args.Player.Kick("Added buff to NPC abnormally.", true); + args.Handled = true; + } + } + + /// The Bouncer handler for when an NPC is rehomed. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnUpdateNPCHome(object sender, GetDataHandlers.NPCHomeChangeEventArgs args) + { + int id = args.ID; + short x = args.X; + short y = args.Y; + byte homeless = args.Homeless; + + if (!args.Player.HasBuildPermission(x, y)) + { + args.Player.SendData(PacketTypes.UpdateNPCHome, "", id, Main.npc[id].homeTileX, Main.npc[id].homeTileY, + Convert.ToByte(Main.npc[id].homeless)); + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(x, y)) + { + args.Player.SendData(PacketTypes.UpdateNPCHome, "", id, Main.npc[id].homeTileX, Main.npc[id].homeTileY, + Convert.ToByte(Main.npc[id].homeless)); + args.Handled = true; + return; + } + } + + /// Bouncer's HealOther handler prevents gross misuse of HealOther packets by hackers. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnHealOtherPlayer(object sender, GetDataHandlers.HealOtherPlayerEventArgs args) + { + short amount = args.Amount; + byte plr = args.TargetPlayerIndex; + + if (amount <= 0 || Main.player[plr] == null || !Main.player[plr].active) + { + args.Handled = true; + return; + } + + // Why 0.2? + // @bartico6: Because heal other player only happens when you are using the spectre armor with the hood, + // and the healing you can do with that is 20% of your damage. + if (amount > TShock.Config.MaxDamage * 0.2) + { + args.Player.Disable("HealOtherPlayer cheat attempt!", DisableFlags.WriteToLogAndConsole); + args.Handled = true; + return; + } + + if (args.Player.HealOtherThreshold > TShock.Config.HealOtherThreshold) + { + args.Player.Disable("Reached HealOtherPlayer threshold.", DisableFlags.WriteToLogAndConsole); + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled() || args.Player.IsBouncerThrottled()) + { + args.Handled = true; + return; + } + + args.Player.HealOtherThreshold++; + args.Handled = false; + return; + } + + /// Bouncer's PlaceObject hook reverts malicious tile placement. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnPlaceObject(object sender, GetDataHandlers.PlaceObjectEventArgs args) + { + short x = args.X; + short y = args.Y; + short type = args.Type; + short style = args.Style; + byte alternate = args.Alternate; + bool direction = args.Direction; + + if (type < 0 || type >= Main.maxTileSets) + { + args.Handled = true; + return; + } + + if (x < 0 || x >= Main.maxTilesX) + { + args.Handled = true; + return; + } + + if (y < 0 || y >= Main.maxTilesY) + { + args.Handled = true; + return; + } + + //style 52 and 53 are used by ItemID.Fake_newchest1 and ItemID.Fake_newchest2 + //These two items cause localised lag and rendering issues + if (type == TileID.FakeContainers && (style == 52 || style == 53)) + { + args.Player.SendTileSquare(x, y, 4); + args.Handled = true; + return; + } + + // TODO: REMOVE. This does NOT look like Bouncer code. + if (TShock.TileBans.TileIsBanned(type, args.Player)) + { + args.Player.SendTileSquare(x, y, 1); + args.Player.SendErrorMessage("You do not have permission to place this tile."); + args.Handled = true; + return; + } + + if (!TShock.Utils.TilePlacementValid(x, y)) + { + args.Player.SendTileSquare(x, y, 1); + args.Handled = true; + return; + } + + if (args.Player.Dead && TShock.Config.PreventDeadModification) + { + args.Player.SendTileSquare(x, y, 4); + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendTileSquare(x, y, 4); + args.Handled = true; + return; + } + + // This is neccessary to check in order to prevent special tiles such as + // queen bee larva, paintings etc that use this packet from being placed + // without selecting the right item. + if (type != args.Player.TPlayer.inventory[args.Player.TPlayer.selectedItem].createTile) + { + args.Player.SendTileSquare(x, y, 4); + args.Handled = true; + return; + } + + TileObjectData tileData = TileObjectData.GetTileData(type, style, 0); + if (tileData == null) + { + args.Handled = true; + return; + } + + x -= tileData.Origin.X; + y -= tileData.Origin.Y; + + for (int i = x; i < x + tileData.Width; i++) + { + for (int j = y; j < y + tileData.Height; j++) + { + if (!args.Player.HasModifiedIceSuccessfully(i, j, type, EditAction.PlaceTile) + && !args.Player.HasBuildPermission(i, j)) + { + args.Player.SendTileSquare(i, j, 4); + args.Handled = true; + return; + } + } + } + + // Ignore rope placement range + if ((type != TileID.Rope + || type != TileID.SilkRope + || type != TileID.VineRope + || type != TileID.WebRope) + && !args.Player.IsInRange(x, y)) + { + args.Player.SendTileSquare(x, y, 4); + args.Handled = true; + return; + } + + if (args.Player.TilePlaceThreshold >= TShock.Config.TilePlaceThreshold) + { + args.Player.Disable("Reached TilePlace threshold.", DisableFlags.WriteToLogAndConsole); + args.Player.SendTileSquare(x, y, 4); + args.Handled = true; + return; + } + + if (!args.Player.HasPermission(Permissions.ignoreplacetiledetection)) + { + args.Player.TilePlaceThreshold++; + var coords = new Vector2(x, y); + lock (args.Player.TilesCreated) + if (!args.Player.TilesCreated.ContainsKey(coords)) + args.Player.TilesCreated.Add(coords, Main.tile[x, y]); + } + } + + /// Fired when a PlaceTileEntity occurs for basic anti-cheat on perms and range. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnPlaceTileEntity(object sender, GetDataHandlers.PlaceTileEntityEventArgs args) + { + if (args.Player.IsBeingDisabled()) + { + args.Handled = true; + return; + } + + if (!args.Player.HasBuildPermission(args.X, args.Y)) + { + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(args.X, args.Y)) + { + args.Handled = true; + return; + } + } + + /// Fired when an item frame is placed for anti-cheat detection. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnPlaceItemFrame(object sender, GetDataHandlers.PlaceItemFrameEventArgs args) + { + if (args.Player.IsBeingDisabled()) + { + NetMessage.SendData((int)PacketTypes.UpdateTileEntity, -1, -1, NetworkText.Empty, args.ItemFrame.ID, 0, 1); + args.Handled = true; + return; + } + + if (!args.Player.HasBuildPermission(args.X, args.Y)) + { + NetMessage.SendData((int)PacketTypes.UpdateTileEntity, -1, -1, NetworkText.Empty, args.ItemFrame.ID, 0, 1); + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(args.X, args.Y)) + { + NetMessage.SendData((int)PacketTypes.UpdateTileEntity, -1, -1, NetworkText.Empty, args.ItemFrame.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 + //belongs to the player who sent the packet. + if (args.Player.Index != args.TargetPlayerIndex) + { + //If the player who sent the packet is not the player being teleported, cancel this packet + args.Player.Disable("Malicious portal attempt.", DisableFlags.WriteToLogAndConsole); //Todo: this message is not particularly clear - suggestions wanted + args.Handled = true; + return; + } + + //Generic bounds checking, though I'm not sure if anyone would willingly hack themselves outside the map? + if (args.NewPosition.X > Main.maxTilesX || args.NewPosition.X < 0 + || args.NewPosition.Y > Main.maxTilesY || args.NewPosition.Y < 0) + { + args.Handled = true; + return; + } + + //May as well reject teleport attempts if the player is being throttled + if (args.Player.IsBeingDisabled() || args.Player.IsBouncerThrottled()) + { + args.Handled = true; + return; + } + } + + /// Handles the anti-cheat components of gem lock toggles. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnGemLockToggle(object sender, GetDataHandlers.GemLockToggleEventArgs args) + { + if (args.X < 0 || args.Y < 0 || args.X >= Main.maxTilesX || args.Y >= Main.maxTilesY) + { + args.Handled = true; + return; + } + + if (!TShock.Utils.TilePlacementValid(args.X, args.Y) || (args.Player.Dead && TShock.Config.PreventDeadModification)) + { + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Handled = true; + return; + } + + if (!args.Player.HasBuildPermission(args.X, args.Y)) + { + args.Handled = true; + return; + } + } + + /// Handles validation of of basic anti-cheat on mass wire operations. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnMassWireOperation(object sender, GetDataHandlers.MassWireOperationEventArgs args) + { + short startX = args.StartX; + short startY = args.StartY; + short endX = args.EndX; + short endY = args.EndY; + + List points = Utils.Instance.GetMassWireOperationRange( + new Point(startX, startY), + new Point(endX, endY), + args.Player.TPlayer.direction == 1); + + int x; + int y; + foreach (Point p in points) + { + /* Perform similar checks to TileKill + * The server-side nature of this packet removes the need to use SendTileSquare + * Range checks are currently ignored here as the items that send this seem to have infinite range */ + + x = p.X; + y = p.Y; + + if (!TShock.Utils.TilePlacementValid(x, y) || (args.Player.Dead && TShock.Config.PreventDeadModification)) + { + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Handled = true; + return; + } + + if (!args.Player.HasBuildPermission(x, y)) + { + args.Handled = true; + return; + } + } + } + + /// Called when a player is damaged. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnPlayerDamage(object sender, GetDataHandlers.PlayerDamageEventArgs args) + { + byte id = args.ID; + short damage = args.Damage; + bool pvp = args.PVP; + bool crit = args.Critical; + byte direction = args.Direction; + + if (id >= Main.maxPlayers || TShock.Players[id] == null) + { + args.Handled = true; + return; + } + + if (damage > TShock.Config.MaxDamage && !args.Player.HasPermission(Permissions.ignoredamagecap) && id != args.Player.Index) + { + if (TShock.Config.KickOnDamageThresholdBroken) + { + args.Player.Kick(string.Format("Player damage exceeded {0}.", TShock.Config.MaxDamage)); + args.Handled = true; + return; + } + else + { + args.Player.Disable(String.Format("Player damage exceeded {0}.", TShock.Config.MaxDamage), DisableFlags.WriteToLogAndConsole); + } + args.Player.SendData(PacketTypes.PlayerHp, "", id); + args.Player.SendData(PacketTypes.PlayerUpdate, "", id); + args.Handled = true; + return; + } + + if (!TShock.Players[id].TPlayer.hostile && pvp && id != args.Player.Index) + { + args.Player.SendData(PacketTypes.PlayerHp, "", id); + args.Player.SendData(PacketTypes.PlayerUpdate, "", id); + args.Handled = true; + return; + } + + if (args.Player.IsBeingDisabled()) + { + args.Player.SendData(PacketTypes.PlayerHp, "", id); + args.Player.SendData(PacketTypes.PlayerUpdate, "", id); + args.Handled = true; + return; + } + + if (!args.Player.IsInRange(TShock.Players[id].TileX, TShock.Players[id].TileY, 100)) + { + args.Player.SendData(PacketTypes.PlayerHp, "", id); + args.Player.SendData(PacketTypes.PlayerUpdate, "", id); + args.Handled = true; + return; + } + + if (args.Player.IsBouncerThrottled()) + { + args.Player.SendData(PacketTypes.PlayerHp, "", id); + args.Player.SendData(PacketTypes.PlayerUpdate, "", id); + args.Handled = true; + return; + } + + } + + /// Bouncer's KillMe hook stops crash exploits from out of bounds values. + /// The object that triggered the event. + /// The packet arguments that the event has. + internal void OnKillMe(object sender, GetDataHandlers.KillMeEventArgs args) + { + short damage = args.Damage; + short id = args.PlayerId; + PlayerDeathReason playerDeathReason = args.PlayerDeathReason; + + if (damage > 20000) //Abnormal values have the potential to cause infinite loops in the server. + { + args.Player.Kick("Failed to shade polygon normals.", true, true); + TShock.Log.ConsoleError("Death Exploit Attempt: Damage {0}", damage); + args.Handled = true; + return; + } + + if (id >= Main.maxPlayers) + { + args.Handled = true; + return; + } + + // This was formerly marked as a crash check; does not actually crash on this specific packet. + if (playerDeathReason != null) + { + if (playerDeathReason.GetDeathText(TShock.Players[id].Name).ToString().Length > 500) + { + TShock.Players[id].Kick("Death reason outside of normal bounds.", true); + args.Handled = true; + return; + } + } + } + + + private static Dictionary NPCAddBuffTimeMax = new Dictionary() + { + { BuffID.Poisoned, 3600 }, + { BuffID.OnFire, 1200 }, + { BuffID.CursedInferno, 420 }, + { BuffID.Frostburn, 900 }, + { BuffID.Ichor, 1200 }, + { BuffID.Venom, 1260 }, + { BuffID.Midas, 120 }, + { BuffID.Wet, 1500 }, + { BuffID.Slimed, 1500 }, + { BuffID.Lovestruck, 1800 }, + { BuffID.Stinky, 1800 }, + { BuffID.SoulDrain, 30 }, + { BuffID.ShadowFlame, 660 }, + { BuffID.DryadsWard, 120 }, + { BuffID.BoneJavelin, 900 }, + { BuffID.StardustMinionBleed, 900 }, + { BuffID.DryadsWardDebuff, 120 }, + { BuffID.BetsysCurse, 600 }, + { BuffID.Oiled, 540 }, + { BuffID.Confused, 450 }, // Brain of Confusion Internal Item ID: 3223 + { BuffID.Daybreak, 300 } // Solar Eruption Item ID: 3473, Daybreak Item ID: 3543 + }; + + /// + /// Tile IDs that can be oriented: + /// Cannon, + /// Chairs, + /// Beds, + /// Bathtubs, + /// Statues, + /// Mannequin, + /// Traps, + /// MusicBoxes, + /// ChristmasTree, + /// WaterFountain, + /// Womannequin, + /// MinecartTrack, + /// WeaponsRack, + /// LunarMonolith, + /// TargetDummy, + /// Campfire + /// + private static int[] orientableTiles = new int[] + { + TileID.Cannon, + TileID.Chairs, + TileID.Beds, + TileID.Bathtubs, + TileID.Statues, + TileID.Mannequin, + TileID.Traps, + TileID.MusicBoxes, + TileID.ChristmasTree, + TileID.WaterFountain, + TileID.Womannequin, + TileID.MinecartTrack, + TileID.WeaponsRack, + TileID.ItemFrame, + TileID.LunarMonolith, + TileID.TargetDummy, + TileID.Campfire + }; + + } +} diff --git a/TShockAPI/CLI/CommandLineParser.cs b/TShockAPI/CLI/CommandLineParser.cs index 7106abbe..d62917fb 100644 --- a/TShockAPI/CLI/CommandLineParser.cs +++ b/TShockAPI/CLI/CommandLineParser.cs @@ -1,3 +1,21 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2011-2019 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.ComponentModel; @@ -165,10 +183,16 @@ namespace TShockAPI.CLI { _source = source; - for (int i = 0; i < (source.Length - 1 == 0 ? 1 : source.Length - 1); i++) + for (int i = 0; i < (source.Length - 1 == 0 ? 1 : source.Length); i++) { string flag = source[i].ToLowerInvariant(); string argument = null; + + if (string.IsNullOrWhiteSpace(flag)) + { + continue; + } + if (i + 1 < source.Length) { argument = source[i + 1]; diff --git a/TShockAPI/CLI/FlagSet.cs b/TShockAPI/CLI/FlagSet.cs index 71f4e07a..ed559f3d 100644 --- a/TShockAPI/CLI/FlagSet.cs +++ b/TShockAPI/CLI/FlagSet.cs @@ -1,3 +1,21 @@ +/* +TShock, a server mod for Terraria +Copyright (C) 2011-2019 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; diff --git a/TShockAPI/Commands.cs b/TShockAPI/Commands.cs index ee9106fc..3af965a8 100644 --- a/TShockAPI/Commands.cs +++ b/TShockAPI/Commands.cs @@ -1,6 +1,6 @@ /* TShock, a server mod for Terraria -Copyright (C) 2011-2017 Nyx Studios (fka. The TShock Team) +Copyright (C) 2011-2019 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 @@ -35,6 +35,7 @@ using Terraria.GameContent.Events; using Microsoft.Xna.Framework; using OTAPI.Tile; using TShockAPI.Localization; +using System.Text.RegularExpressions; namespace TShockAPI { @@ -192,11 +193,17 @@ namespace TShockAPI public static List ChatCommands = new List(); public static ReadOnlyCollection TShockCommands = new ReadOnlyCollection(new List()); + /// + /// The command specifier, defaults to "/" + /// public static string Specifier { get { return string.IsNullOrWhiteSpace(TShock.Config.CommandSpecifier) ? "/" : TShock.Config.CommandSpecifier; } } + /// + /// The silent command specifier, defaults to "." + /// public static string SilentSpecifier { get { return string.IsNullOrWhiteSpace(TShock.Config.CommandSilentSpecifier) ? "." : TShock.Config.CommandSilentSpecifier; } @@ -213,7 +220,7 @@ namespace TShockAPI ChatCommands.Add(cmd); }; - add(new Command(AuthToken, "auth") + add(new Command(SetupToken, "setup") { AllowServer = false, HelpText = "Used to authenticate as superadmin when first setting up TShock." @@ -311,6 +318,14 @@ namespace TShockAPI { HelpText = "Temporarily sets another player's group." }); + add(new Command(Permissions.su, SubstituteUser, "su") + { + HelpText = "Temporarily elevates you to Super Admin." + }); + add(new Command(Permissions.su, SubstituteUserDo, "sudo") + { + HelpText = "Executes a command as the super admin." + }); add(new Command(Permissions.userinfo, GrabUserUserInfo, "userinfo", "ui") { HelpText = "Shows information about a player." @@ -351,10 +366,6 @@ namespace TShockAPI { HelpText = "Reloads the server configuration file." }); - add(new Command(Permissions.maintenance, Restart, "restart") - { - HelpText = "Restarts the server." - }); add(new Command(Permissions.cfgpassword, ServerPassword, "serverpassword") { HelpText = "Changes the server password." @@ -601,6 +612,10 @@ namespace TShockAPI { HelpText = "Sends a PM to a player." }); + add(new Command(Permissions.createdumps, CreateDumps, "dump-reference-data") + { + HelpText = "Creates a reference tables for Terraria data types and the TShock permission system in the server folder." + }); #endregion add(new Command(Aliases, "aliases") @@ -685,6 +700,10 @@ namespace TShockAPI { TShock.Utils.SendLogs(string.Format("{0} tried to execute {1}{2}.", player.Name, Specifier, cmdText), Color.PaleVioletRed, player); player.SendErrorMessage("You do not have access to this command."); + if (player.HasPermission(Permissions.su)) + { + player.SendInfoMessage("You can use '{0}sudo {0}{1}' to override this check.", Specifier, cmdText); + } } else if (!cmd.AllowServer && !player.RealPlayer) { @@ -764,7 +783,7 @@ namespace TShockAPI { TShock.Log.Warn(String.Format("{0} ({1}) had {2} or more invalid login attempts and was kicked automatically.", args.Player.IP, args.Player.Name, TShock.Config.MaximumLoginAttempts)); - TShock.Utils.Kick(args.Player, "Too many invalid login attempts."); + args.Player.Kick("Too many invalid login attempts."); return; } @@ -774,7 +793,7 @@ namespace TShockAPI return; } - User user = TShock.Users.GetUserByName(args.Player.Name); + UserAccount account = TShock.UserAccounts.GetUserAccountByName(args.Player.Name); string password = ""; bool usingUUID = false; if (args.Parameters.Count == 0 && !TShock.Config.DisableUUIDLogin) @@ -800,7 +819,7 @@ namespace TShockAPI if (PlayerHooks.OnPlayerPreLogin(args.Player, args.Parameters[0], args.Parameters[1])) return; - user = TShock.Users.GetUserByName(args.Parameters[0]); + account = TShock.UserAccounts.GetUserAccountByName(args.Parameters[0]); password = args.Parameters[1]; } else @@ -813,23 +832,23 @@ namespace TShockAPI } try { - if (user == null) + if (account == null) { - args.Player.SendErrorMessage("A user by that name does not exist."); + args.Player.SendErrorMessage("A user account by that name does not exist."); } - else if (user.VerifyPassword(password) || - (usingUUID && user.UUID == args.Player.UUID && !TShock.Config.DisableUUIDLogin && + else if (account.VerifyPassword(password) || + (usingUUID && account.UUID == args.Player.UUID && !TShock.Config.DisableUUIDLogin && !String.IsNullOrWhiteSpace(args.Player.UUID))) { - args.Player.PlayerData = TShock.CharacterDB.GetPlayerData(args.Player, user.ID); + args.Player.PlayerData = TShock.CharacterDB.GetPlayerData(args.Player, account.ID); - var group = TShock.Utils.GetGroup(user.Group); + var group = TShock.Groups.GetGroupByName(account.Group); args.Player.Group = group; args.Player.tempGroup = null; - args.Player.User = user; + args.Player.Account = account; args.Player.IsLoggedIn = true; - args.Player.IgnoreActionsForInventory = "none"; + args.Player.IsDisabledForSSC = false; if (Main.ServerSideCharacter) { @@ -843,14 +862,14 @@ namespace TShockAPI args.Player.LoginFailsBySsi = false; if (args.Player.HasPermission(Permissions.ignorestackhackdetection)) - args.Player.IgnoreActionsForCheating = "none"; + args.Player.IsDisabledForStackDetection = false; if (args.Player.HasPermission(Permissions.usebanneditem)) - args.Player.IgnoreActionsForDisabledArmor = "none"; + args.Player.IsDisabledForBannedWearable = false; - args.Player.SendSuccessMessage("Authenticated as " + user.Name + " successfully."); + args.Player.SendSuccessMessage("Authenticated as " + account.Name + " successfully."); - TShock.Log.ConsoleInfo(args.Player.Name + " authenticated successfully as user: " + user.Name + "."); + TShock.Log.ConsoleInfo(args.Player.Name + " authenticated successfully as user: " + account.Name + "."); if ((args.Player.LoginHarassed) && (TShock.Config.RememberLeavePos)) { if (TShock.RememberedPos.GetLeavePos(args.Player.Name, args.Player.IP) != Vector2.Zero) @@ -861,7 +880,7 @@ namespace TShockAPI args.Player.LoginHarassed = false; } - TShock.Users.SetUserUUID(user, args.Player.UUID); + TShock.UserAccounts.SetUserAccountUUID(account, args.Player.UUID); Hooks.PlayerHooks.OnPlayerPostLogin(args.Player); } @@ -875,7 +894,7 @@ namespace TShockAPI { args.Player.SendErrorMessage("Invalid password!"); } - TShock.Log.Warn(args.Player.IP + " failed to authenticate as user: " + user.Name + "."); + TShock.Log.Warn(args.Player.IP + " failed to authenticate as user: " + account.Name + "."); args.Player.LoginAttempts++; } } @@ -909,14 +928,14 @@ namespace TShockAPI if (args.Player.IsLoggedIn && args.Parameters.Count == 2) { string password = args.Parameters[0]; - if (args.Player.User.VerifyPassword(password)) + if (args.Player.Account.VerifyPassword(password)) { try { args.Player.SendSuccessMessage("You changed your password!"); - TShock.Users.SetUserPassword(args.Player.User, args.Parameters[1]); // SetUserPassword will hash it for you. + TShock.UserAccounts.SetUserAccountPassword(args.Player.Account, args.Parameters[1]); // SetUserPassword will hash it for you. TShock.Log.ConsoleInfo(args.Player.IP + " named " + args.Player.Name + " changed the password of account " + - args.Player.User.Name + "."); + args.Player.Account.Name + "."); } catch (ArgumentOutOfRangeException) { @@ -927,7 +946,7 @@ namespace TShockAPI { args.Player.SendErrorMessage("You failed to change your password!"); TShock.Log.ConsoleError(args.Player.IP + " named " + args.Player.Name + " failed to change password for account: " + - args.Player.User.Name + "."); + args.Player.Account.Name + "."); } } else @@ -935,7 +954,7 @@ namespace TShockAPI args.Player.SendErrorMessage("Not logged in or invalid syntax! Proper syntax: {0}password ", Specifier); } } - catch (UserManagerException ex) + catch (UserAccountManagerException ex) { args.Player.SendErrorMessage("Sorry, an error occured: " + ex.Message + "."); TShock.Log.ConsoleError("PasswordUser returned an error: " + ex); @@ -946,15 +965,15 @@ namespace TShockAPI { try { - var user = new User(); + var account = new UserAccount(); string echoPassword = ""; if (args.Parameters.Count == 1) { - user.Name = args.Player.Name; + account.Name = args.Player.Name; echoPassword = args.Parameters[0]; try { - user.CreateBCryptHash(args.Parameters[0]); + account.CreateBCryptHash(args.Parameters[0]); } catch (ArgumentOutOfRangeException) { @@ -964,11 +983,11 @@ namespace TShockAPI } else if (args.Parameters.Count == 2 && TShock.Config.AllowRegisterAnyUsername) { - user.Name = args.Parameters[0]; + account.Name = args.Parameters[0]; echoPassword = args.Parameters[1]; try { - user.CreateBCryptHash(args.Parameters[1]); + account.CreateBCryptHash(args.Parameters[1]); } catch (ArgumentOutOfRangeException) { @@ -982,24 +1001,24 @@ namespace TShockAPI return; } - user.Group = TShock.Config.DefaultRegistrationGroupName; // FIXME -- we should get this from the DB. --Why? - user.UUID = args.Player.UUID; + account.Group = TShock.Config.DefaultRegistrationGroupName; // FIXME -- we should get this from the DB. --Why? + account.UUID = args.Player.UUID; - if (TShock.Users.GetUserByName(user.Name) == null && user.Name != TSServerPlayer.AccountName) // Cheap way of checking for existance of a user + if (TShock.UserAccounts.GetUserAccountByName(account.Name) == null && account.Name != TSServerPlayer.AccountName) // Cheap way of checking for existance of a user { - args.Player.SendSuccessMessage("Account \"{0}\" has been registered.", user.Name); + args.Player.SendSuccessMessage("Account \"{0}\" has been registered.", account.Name); args.Player.SendSuccessMessage("Your password is {0}.", echoPassword); - TShock.Users.AddUser(user); - TShock.Log.ConsoleInfo("{0} registered an account: \"{1}\".", args.Player.Name, user.Name); + TShock.UserAccounts.AddUserAccount(account); + TShock.Log.ConsoleInfo("{0} registered an account: \"{1}\".", args.Player.Name, account.Name); } else { - args.Player.SendErrorMessage("Sorry, " + user.Name + " was already taken by another person."); + args.Player.SendErrorMessage("Sorry, " + account.Name + " was already taken by another person."); args.Player.SendErrorMessage("Please try a different username."); - TShock.Log.ConsoleInfo(args.Player.Name + " failed to register an existing account: " + user.Name); + TShock.Log.ConsoleInfo(args.Player.Name + " failed to register an existing account: " + account.Name); } } - catch (UserManagerException ex) + catch (UserAccountManagerException ex) { args.Player.SendErrorMessage("Sorry, an error occured: " + ex.Message + "."); TShock.Log.ConsoleError("RegisterUser returned an error: " + ex); @@ -1020,57 +1039,57 @@ namespace TShockAPI // Add requires a username, password, and a group specified. if (subcmd == "add" && args.Parameters.Count == 4) { - var user = new User(); + var account = new UserAccount(); - user.Name = args.Parameters[1]; + account.Name = args.Parameters[1]; try { - user.CreateBCryptHash(args.Parameters[2]); + account.CreateBCryptHash(args.Parameters[2]); } catch (ArgumentOutOfRangeException) { args.Player.SendErrorMessage("Password must be greater than or equal to " + TShock.Config.MinimumPasswordLength + " characters."); return; } - user.Group = args.Parameters[3]; + account.Group = args.Parameters[3]; try { - TShock.Users.AddUser(user); - args.Player.SendSuccessMessage("Account " + user.Name + " has been added to group " + user.Group + "!"); - TShock.Log.ConsoleInfo(args.Player.Name + " added Account " + user.Name + " to group " + user.Group); + TShock.UserAccounts.AddUserAccount(account); + args.Player.SendSuccessMessage("Account " + account.Name + " has been added to group " + account.Group + "!"); + TShock.Log.ConsoleInfo(args.Player.Name + " added Account " + account.Name + " to group " + account.Group); } catch (GroupNotExistsException) { - args.Player.SendErrorMessage("Group " + user.Group + " does not exist!"); + args.Player.SendErrorMessage("Group " + account.Group + " does not exist!"); } - catch (UserExistsException) + catch (UserAccountExistsException) { - args.Player.SendErrorMessage("User " + user.Name + " already exists!"); + args.Player.SendErrorMessage("User " + account.Name + " already exists!"); } - catch (UserManagerException e) + catch (UserAccountManagerException e) { - args.Player.SendErrorMessage("User " + user.Name + " could not be added, check console for details."); + args.Player.SendErrorMessage("User " + account.Name + " could not be added, check console for details."); TShock.Log.ConsoleError(e.ToString()); } } // User deletion requires a username else if (subcmd == "del" && args.Parameters.Count == 2) { - var user = new User(); - user.Name = args.Parameters[1]; + var account = new UserAccount(); + account.Name = args.Parameters[1]; try { - TShock.Users.RemoveUser(user); + TShock.UserAccounts.RemoveUserAccount(account); args.Player.SendSuccessMessage("Account removed successfully."); TShock.Log.ConsoleInfo(args.Player.Name + " successfully deleted account: " + args.Parameters[1] + "."); } - catch (UserNotExistException) + catch (UserAccountNotExistException) { - args.Player.SendErrorMessage("The user " + user.Name + " does not exist! Deleted nobody!"); + args.Player.SendErrorMessage("The user " + account.Name + " does not exist! Deleted nobody!"); } - catch (UserManagerException ex) + catch (UserAccountManagerException ex) { args.Player.SendErrorMessage(ex.Message); TShock.Log.ConsoleError(ex.ToString()); @@ -1080,22 +1099,22 @@ namespace TShockAPI // Password changing requires a username, and a new password to set else if (subcmd == "password" && args.Parameters.Count == 3) { - var user = new User(); - user.Name = args.Parameters[1]; + var account = new UserAccount(); + account.Name = args.Parameters[1]; try { - TShock.Users.SetUserPassword(user, args.Parameters[2]); - TShock.Log.ConsoleInfo(args.Player.Name + " changed the password of account " + user.Name); - args.Player.SendSuccessMessage("Password change succeeded for " + user.Name + "."); + TShock.UserAccounts.SetUserAccountPassword(account, args.Parameters[2]); + TShock.Log.ConsoleInfo(args.Player.Name + " changed the password of account " + account.Name); + args.Player.SendSuccessMessage("Password change succeeded for " + account.Name + "."); } - catch (UserNotExistException) + catch (UserAccountNotExistException) { - args.Player.SendErrorMessage("User " + user.Name + " does not exist!"); + args.Player.SendErrorMessage("User " + account.Name + " does not exist!"); } - catch (UserManagerException e) + catch (UserAccountManagerException e) { - args.Player.SendErrorMessage("Password change for " + user.Name + " failed! Check console!"); + args.Player.SendErrorMessage("Password change for " + account.Name + " failed! Check console!"); TShock.Log.ConsoleError(e.ToString()); } catch (ArgumentOutOfRangeException) @@ -1106,26 +1125,26 @@ namespace TShockAPI // Group changing requires a username or IP address, and a new group to set else if (subcmd == "group" && args.Parameters.Count == 3) { - var user = new User(); - user.Name = args.Parameters[1]; + var account = new UserAccount(); + account.Name = args.Parameters[1]; try { - TShock.Users.SetUserGroup(user, args.Parameters[2]); - TShock.Log.ConsoleInfo(args.Player.Name + " changed account " + user.Name + " to group " + args.Parameters[2] + "."); - args.Player.SendSuccessMessage("Account " + user.Name + " has been changed to group " + args.Parameters[2] + "!"); + TShock.UserAccounts.SetUserGroup(account, args.Parameters[2]); + TShock.Log.ConsoleInfo(args.Player.Name + " changed account " + account.Name + " to group " + args.Parameters[2] + "."); + args.Player.SendSuccessMessage("Account " + account.Name + " has been changed to group " + args.Parameters[2] + "!"); } catch (GroupNotExistsException) { args.Player.SendErrorMessage("That group does not exist!"); } - catch (UserNotExistException) + catch (UserAccountNotExistException) { - args.Player.SendErrorMessage("User " + user.Name + " does not exist!"); + args.Player.SendErrorMessage("User " + account.Name + " does not exist!"); } - catch (UserManagerException e) + catch (UserAccountManagerException e) { - args.Player.SendErrorMessage("User " + user.Name + " could not be added. Check console for details."); + args.Player.SendErrorMessage("User " + account.Name + " could not be added. Check console for details."); TShock.Log.ConsoleError(e.ToString()); } } @@ -1176,17 +1195,17 @@ namespace TShockAPI return; } - var players = TShock.Utils.FindPlayer(args.Parameters[0]); + var players = TSPlayer.FindByNameOrID(args.Parameters[0]); if (players.Count < 1) args.Player.SendErrorMessage("Invalid player."); else if (players.Count > 1) - TShock.Utils.SendMultipleMatchError(args.Player, players.Select(p => p.Name)); + args.Player.SendMultipleMatchError(players.Select(p => p.Name)); else { var message = new StringBuilder(); message.Append("IP Address: ").Append(players[0].IP); - if (players[0].User != null && players[0].IsLoggedIn) - message.Append(" | Logged in as: ").Append(players[0].User.Name).Append(" | Group: ").Append(players[0].Group.Name); + if (players[0].Account != null && players[0].IsLoggedIn) + message.Append(" | Logged in as: ").Append(players[0].Account.Name).Append(" | Group: ").Append(players[0].Group.Name); args.Player.SendSuccessMessage(message.ToString()); } } @@ -1202,28 +1221,28 @@ namespace TShockAPI string username = String.Join(" ", args.Parameters); if (!string.IsNullOrWhiteSpace(username)) { - var user = TShock.Users.GetUserByName(username); - if (user != null) + var account = TShock.UserAccounts.GetUserAccountByName(username); + if (account != null) { DateTime LastSeen; string Timezone = TimeZone.CurrentTimeZone.GetUtcOffset(DateTime.Now).Hours.ToString("+#;-#"); - if (DateTime.TryParse(user.LastAccessed, out LastSeen)) + if (DateTime.TryParse(account.LastAccessed, out LastSeen)) { - LastSeen = DateTime.Parse(user.LastAccessed).ToLocalTime(); - args.Player.SendSuccessMessage("{0}'s last login occured {1} {2} UTC{3}.", user.Name, LastSeen.ToShortDateString(), + LastSeen = DateTime.Parse(account.LastAccessed).ToLocalTime(); + args.Player.SendSuccessMessage("{0}'s last login occured {1} {2} UTC{3}.", account.Name, LastSeen.ToShortDateString(), LastSeen.ToShortTimeString(), Timezone); } if (args.Player.Group.HasPermission(Permissions.advaccountinfo)) { - List KnownIps = JsonConvert.DeserializeObject>(user.KnownIps?.ToString() ?? string.Empty); + List KnownIps = JsonConvert.DeserializeObject>(account.KnownIps?.ToString() ?? string.Empty); string ip = KnownIps?[KnownIps.Count - 1] ?? "N/A"; - DateTime Registered = DateTime.Parse(user.Registered).ToLocalTime(); + DateTime Registered = DateTime.Parse(account.Registered).ToLocalTime(); - args.Player.SendSuccessMessage("{0}'s group is {1}.", user.Name, user.Group); - args.Player.SendSuccessMessage("{0}'s last known IP is {1}.", user.Name, ip); - args.Player.SendSuccessMessage("{0}'s register date is {1} {2} UTC{3}.", user.Name, Registered.ToShortDateString(), Registered.ToShortTimeString(), Timezone); + args.Player.SendSuccessMessage("{0}'s group is {1}.", account.Name, account.Group); + args.Player.SendSuccessMessage("{0}'s last known IP is {1}.", account.Name, ip); + args.Player.SendSuccessMessage("{0}'s register date is {1} {2} UTC{3}.", account.Name, Registered.ToShortDateString(), Registered.ToShortTimeString(), Timezone); } } else @@ -1246,21 +1265,21 @@ namespace TShockAPI } string plStr = args.Parameters[0]; - var players = TShock.Utils.FindPlayer(plStr); + var players = TSPlayer.FindByNameOrID(plStr); if (players.Count == 0) { args.Player.SendErrorMessage("Invalid player!"); } else if (players.Count > 1) { - TShock.Utils.SendMultipleMatchError(args.Player, players.Select(p => p.Name)); + args.Player.SendMultipleMatchError(players.Select(p => p.Name)); } else { string reason = args.Parameters.Count > 1 ? String.Join(" ", args.Parameters.GetRange(1, args.Parameters.Count - 1)) : "Misbehaviour."; - if (!TShock.Utils.Kick(players[0], reason, !args.Player.RealPlayer, false, args.Player.Name)) + if (!players[0].Kick(reason, !args.Player.RealPlayer, false, args.Player.Name)) { args.Player.SendErrorMessage("You can't kick another admin!"); } @@ -1273,200 +1292,199 @@ namespace TShockAPI switch (subcmd) { case "add": - #region Add ban + #region Add Ban { if (args.Parameters.Count < 2) { - args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}ban add [reason]", Specifier); + args.Player.SendErrorMessage("Invalid command. Format: {0}ban add [time] [reason]", Specifier); + args.Player.SendErrorMessage("Example: {0}ban add Shank 10d Hacking and cheating", Specifier); + args.Player.SendErrorMessage("Example: {0}ban add Ash", Specifier); + args.Player.SendErrorMessage("Use the time 0 (zero) for a permanent ban."); return; } - List players = TShock.Utils.FindPlayer(args.Parameters[1]); - string reason = args.Parameters.Count > 2 ? String.Join(" ", args.Parameters.Skip(2)) : "Misbehavior."; - if (players.Count == 0) + // Used only to notify if a ban was successful and who the ban was about + bool success = false; + string targetGeneralizedName = ""; + + // Effective ban target assignment + List players = TSPlayer.FindByNameOrID(args.Parameters[1]); + + // Bad case: Players contains more than 1 person so we can't ban them + if (players.Count > 1) { - var user = TShock.Users.GetUserByName(args.Parameters[1]); - if (user != null) + //Fail fast + args.Player.SendMultipleMatchError(players.Select(p => p.Name)); + return; + } + + UserAccount offlineUserAccount = TShock.UserAccounts.GetUserAccountByName(args.Parameters[1]); + + // Storage variable to determine if the command executor is the server console + // If it is, we assume they have full control and let them override permission checks + bool callerIsServerConsole = args.Player is TSServerPlayer; + + // The ban reason the ban is going to have + string banReason = "Unknown."; + + // The default ban length + // 0 is permanent ban, otherwise temp ban + int banLengthInSeconds = 0; + + // Figure out if param 2 is a time or 0 or garbage + if (args.Parameters.Count >= 3) + { + bool parsedOkay = false; + if (args.Parameters[2] != "0") { - bool force = !args.Player.RealPlayer; - - if (user.Name == args.Player.Name && !force) - { - args.Player.SendErrorMessage("You can't ban yourself!"); - return; - } - - if (TShock.Groups.GetGroupByName(user.Group).HasPermission(Permissions.immunetoban) && !force) - args.Player.SendErrorMessage("You can't ban {0}!", user.Name); - else - { - if (user.KnownIps == null) - { - args.Player.SendErrorMessage("Cannot ban {0} because they have no IPs to ban.", user.Name); - return; - } - var knownIps = JsonConvert.DeserializeObject>(user.KnownIps); - TShock.Bans.AddBan(knownIps.Last(), user.Name, user.UUID, reason, false, args.Player.User.Name); - if (String.IsNullOrWhiteSpace(args.Player.User.Name)) - { - if (args.Silent) - { - args.Player.SendInfoMessage("{0} was {1}banned for '{2}'.", user.Name, force ? "Force " : "", reason); - } - else - { - TSPlayer.All.SendInfoMessage("{0} was {1}banned for '{2}'.", user.Name, force ? "Force " : "", reason); - } - } - else - { - if (args.Silent) - { - args.Player.SendInfoMessage("{0}banned {1} for '{2}'.", force ? "Force " : "", user.Name, reason); - } - else - { - TSPlayer.All.SendInfoMessage("{0} {1}banned {2} for '{3}'.", args.Player.Name, force ? "Force " : "", user.Name, reason); - } - } - } + parsedOkay = TShock.Utils.TryParseTime(args.Parameters[2], out banLengthInSeconds); + } + else + { + parsedOkay = true; } - else - args.Player.SendErrorMessage("Invalid player or account!"); - } - else if (players.Count > 1) - TShock.Utils.SendMultipleMatchError(args.Player, players.Select(p => p.Name)); - else - { - if (!TShock.Utils.Ban(players[0], reason, !args.Player.RealPlayer, args.Player.User.Name)) - args.Player.SendErrorMessage("You can't ban {0}!", players[0].Name); - } - } - #endregion - return; - case "addip": - #region Add IP ban - { - if (args.Parameters.Count < 2) - { - args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}ban addip [reason]", Specifier); - return; - } - string ip = args.Parameters[1]; - string reason = args.Parameters.Count > 2 - ? String.Join(" ", args.Parameters.GetRange(2, args.Parameters.Count - 2)) - : "Manually added IP address ban."; - TShock.Bans.AddBan(ip, "", "", reason, false, args.Player.User.Name); - args.Player.SendSuccessMessage("Banned IP {0}.", ip); - } - #endregion - return; - case "addtemp": - #region Add temp ban - { - if (args.Parameters.Count < 3) - { - args.Player.SendErrorMessage("Invalid syntax! Proper syntax: {0}ban addtemp