From 58eb32c7ed192890b9ef72c7c6e48c066d4dd6d3 Mon Sep 17 00:00:00 2001 From: LeonardoVAC Date: Wed, 14 Oct 2026 23:22:54 -0300 Subject: [PATCH] feat: Add TerraSharp for Terraria and tModLoader --- .editorconfig | 55 ++ .gitattributes | 2 + .gitignore | 29 + Directory.Build.props | 19 + LICENSE | 21 + README.md | 70 ++ TerraSharp.slnx | 19 + assets/menu.png | Bin 0 -> 242662 bytes autoload.bat | 203 ++++++ build-and-inject.bat | 73 ++ build.bat | 98 +++ src/TerraSharp.Injector/bootstrap-x64.asm | 93 +++ src/TerraSharp.Injector/bootstrap-x86.asm | 134 ++++ src/TerraSharp.Injector/bootstrap.h | 55 ++ src/TerraSharp.Injector/embed-bootstrap.ps1 | 155 +++++ src/TerraSharp.Injector/injector.cpp | 574 ++++++++++++++++ src/TerraSharp/Autoload/Autoload.cs | 56 ++ src/TerraSharp/Autoload/DomainManager.cs | 22 + src/TerraSharp/Core/Clock.cs | 9 + src/TerraSharp/Core/GameMouse.cs | 19 + src/TerraSharp/Core/Hotkeys.cs | 166 +++++ src/TerraSharp/Core/Json.cs | 359 ++++++++++ src/TerraSharp/Core/Log.cs | 62 ++ src/TerraSharp/Core/Plugin.cs | 119 ++++ src/TerraSharp/Core/Profiler.cs | 120 ++++ src/TerraSharp/Core/Settings.Json.cs | 134 ++++ src/TerraSharp/Core/Settings.cs | 226 +++++++ src/TerraSharp/Entry.cs | 69 ++ src/TerraSharp/Features/AutoFishing.cs | 58 ++ src/TerraSharp/Features/Combat/AimOverride.cs | 24 + src/TerraSharp/Features/Combat/Aimbot.cs | 438 ++++++++++++ .../Features/Combat/CombatFeatures.cs | 102 +++ src/TerraSharp/Features/Combat/Hostility.cs | 12 + src/TerraSharp/Features/Combat/NpcMotion.cs | 241 +++++++ src/TerraSharp/Features/Combat/ShotPreview.cs | 42 ++ src/TerraSharp/Features/HeldItemOverrides.cs | 64 ++ src/TerraSharp/Features/ItemFeatures.cs | 27 + src/TerraSharp/Features/PlayerFeatures.cs | 289 ++++++++ src/TerraSharp/Features/Waypoints.cs | 56 ++ src/TerraSharp/Features/WorldFeatures.cs | 122 ++++ src/TerraSharp/Patches/Guard.cs | 31 + src/TerraSharp/Patches/HookRegistry.cs | 63 ++ src/TerraSharp/Patches/MainPatches.cs | 162 +++++ src/TerraSharp/Patches/NoSpreadPatch.cs | 90 +++ src/TerraSharp/Patches/NpcPatches.cs | 21 + src/TerraSharp/Patches/PlayerPatches.cs | 101 +++ src/TerraSharp/Patches/SandboxGuards.cs | 120 ++++ src/TerraSharp/Polyfills/IsExternalInit.cs | 6 + src/TerraSharp/Simulation/Ballistics.cs | 382 +++++++++++ src/TerraSharp/Simulation/Sandbox.cs | 640 ++++++++++++++++++ src/TerraSharp/Simulation/TileCollision.cs | 49 ++ src/TerraSharp/StartupHook.cs | 13 + src/TerraSharp/TerraSharp.csproj | 50 ++ src/TerraSharp/UI/BuffBrowser.cs | 122 ++++ src/TerraSharp/UI/GameTextures.cs | 89 +++ src/TerraSharp/UI/GridBrowser.cs | 123 ++++ src/TerraSharp/UI/IdIndex.cs | 55 ++ src/TerraSharp/UI/InterfaceLayers.cs | 174 +++++ src/TerraSharp/UI/ItemBrowser.cs | 165 +++++ src/TerraSharp/UI/MainWindow.Pages.cs | 288 ++++++++ src/TerraSharp/UI/MainWindow.Tooltips.cs | 79 +++ src/TerraSharp/UI/MainWindow.cs | 325 +++++++++ src/TerraSharp/UI/Render.cs | 146 ++++ src/TerraSharp/UI/Scroll.cs | 57 ++ src/TerraSharp/UI/TextField.cs | 8 + src/TerraSharp/UI/Widgets.cs | 334 +++++++++ src/TerraSharp/UI/Window.cs | 28 + src/TerraSharp/UI/WindowManager.cs | 196 ++++++ src/TerraSharp/Visuals/AimOverlay.cs | 81 +++ src/TerraSharp/Visuals/Esp.cs | 244 +++++++ 70 files changed, 8678 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Directory.Build.props create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TerraSharp.slnx create mode 100644 assets/menu.png create mode 100644 autoload.bat create mode 100644 build-and-inject.bat create mode 100644 build.bat create mode 100644 src/TerraSharp.Injector/bootstrap-x64.asm create mode 100644 src/TerraSharp.Injector/bootstrap-x86.asm create mode 100644 src/TerraSharp.Injector/bootstrap.h create mode 100644 src/TerraSharp.Injector/embed-bootstrap.ps1 create mode 100644 src/TerraSharp.Injector/injector.cpp create mode 100644 src/TerraSharp/Autoload/Autoload.cs create mode 100644 src/TerraSharp/Autoload/DomainManager.cs create mode 100644 src/TerraSharp/Core/Clock.cs create mode 100644 src/TerraSharp/Core/GameMouse.cs create mode 100644 src/TerraSharp/Core/Hotkeys.cs create mode 100644 src/TerraSharp/Core/Json.cs create mode 100644 src/TerraSharp/Core/Log.cs create mode 100644 src/TerraSharp/Core/Plugin.cs create mode 100644 src/TerraSharp/Core/Profiler.cs create mode 100644 src/TerraSharp/Core/Settings.Json.cs create mode 100644 src/TerraSharp/Core/Settings.cs create mode 100644 src/TerraSharp/Entry.cs create mode 100644 src/TerraSharp/Features/AutoFishing.cs create mode 100644 src/TerraSharp/Features/Combat/AimOverride.cs create mode 100644 src/TerraSharp/Features/Combat/Aimbot.cs create mode 100644 src/TerraSharp/Features/Combat/CombatFeatures.cs create mode 100644 src/TerraSharp/Features/Combat/Hostility.cs create mode 100644 src/TerraSharp/Features/Combat/NpcMotion.cs create mode 100644 src/TerraSharp/Features/Combat/ShotPreview.cs create mode 100644 src/TerraSharp/Features/HeldItemOverrides.cs create mode 100644 src/TerraSharp/Features/ItemFeatures.cs create mode 100644 src/TerraSharp/Features/PlayerFeatures.cs create mode 100644 src/TerraSharp/Features/Waypoints.cs create mode 100644 src/TerraSharp/Features/WorldFeatures.cs create mode 100644 src/TerraSharp/Patches/Guard.cs create mode 100644 src/TerraSharp/Patches/HookRegistry.cs create mode 100644 src/TerraSharp/Patches/MainPatches.cs create mode 100644 src/TerraSharp/Patches/NoSpreadPatch.cs create mode 100644 src/TerraSharp/Patches/NpcPatches.cs create mode 100644 src/TerraSharp/Patches/PlayerPatches.cs create mode 100644 src/TerraSharp/Patches/SandboxGuards.cs create mode 100644 src/TerraSharp/Polyfills/IsExternalInit.cs create mode 100644 src/TerraSharp/Simulation/Ballistics.cs create mode 100644 src/TerraSharp/Simulation/Sandbox.cs create mode 100644 src/TerraSharp/Simulation/TileCollision.cs create mode 100644 src/TerraSharp/StartupHook.cs create mode 100644 src/TerraSharp/TerraSharp.csproj create mode 100644 src/TerraSharp/UI/BuffBrowser.cs create mode 100644 src/TerraSharp/UI/GameTextures.cs create mode 100644 src/TerraSharp/UI/GridBrowser.cs create mode 100644 src/TerraSharp/UI/IdIndex.cs create mode 100644 src/TerraSharp/UI/InterfaceLayers.cs create mode 100644 src/TerraSharp/UI/ItemBrowser.cs create mode 100644 src/TerraSharp/UI/MainWindow.Pages.cs create mode 100644 src/TerraSharp/UI/MainWindow.Tooltips.cs create mode 100644 src/TerraSharp/UI/MainWindow.cs create mode 100644 src/TerraSharp/UI/Render.cs create mode 100644 src/TerraSharp/UI/Scroll.cs create mode 100644 src/TerraSharp/UI/TextField.cs create mode 100644 src/TerraSharp/UI/Widgets.cs create mode 100644 src/TerraSharp/UI/Window.cs create mode 100644 src/TerraSharp/UI/WindowManager.cs create mode 100644 src/TerraSharp/Visuals/AimOverlay.cs create mode 100644 src/TerraSharp/Visuals/Esp.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..421fa41 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,55 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{csproj,props,targets,sln,json,yml}] +indent_size = 2 + +[*.cs] +max_line_length = off + +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:suggestion +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:suggestion +csharp_prefer_braces = false:silent +csharp_new_line_before_open_brace = all +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_sort_system_directives_first = true +dotnet_style_require_accessibility_modifiers = omit_if_default:suggestion + +# Static readonly fields and constants use PascalCase; other private fields use _camelCase. +dotnet_naming_rule.static_readonly_fields.symbols = static_readonly_fields +dotnet_naming_rule.static_readonly_fields.style = pascal +dotnet_naming_rule.static_readonly_fields.severity = suggestion +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.applicable_accessibilities = private +dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly + +dotnet_naming_rule.constants.symbols = constants +dotnet_naming_rule.constants.style = pascal +dotnet_naming_rule.constants.severity = suggestion +dotnet_naming_symbols.constants.applicable_kinds = field +dotnet_naming_symbols.constants.required_modifiers = const +dotnet_naming_style.pascal.capitalization = pascal_case + +dotnet_naming_rule.private_fields.symbols = private_fields +dotnet_naming_rule.private_fields.style = underscore_camel +dotnet_naming_rule.private_fields.severity = suggestion +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_style.underscore_camel.capitalization = camel_case +dotnet_naming_style.underscore_camel.required_prefix = _ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..cc23192 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +*.bat text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d84b3b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Build output +artifacts/ +bin/ +obj/ +run/ + +# IDEs +.vs/ +.idea/ +.vscode/ +*.user +*.suo +*.rsuser +*.userprefs +*.DotSettings.user + +# Native build +*.obj +*.pdb +*.ilk +*.exp +*.iobj +*.ipdb + +# Logs and temp +*.log +*.tmp +*.swp +*~ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..72e27f1 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,19 @@ + + + + net48;net8.0 + x86 + x64 + latest + enable + disable + false + false + true + true + C:\Program Files (x86)\Steam\steamapps\common\Terraria + C:\Program Files (x86)\Steam\steamapps\common\tModLoader + C:\Windows\Microsoft.NET\assembly\GAC_32 + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f5ca5fe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 leonardovac + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..12471c8 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +
+ +# TerraSharp + +TSharp / T# + +An internal cheat menu for Terraria and tModLoader. + +C# +Windows +Terraria 1.4.5 +tModLoader supported +MIT License + +TerraSharp Player menu in Terraria with ESP enabled + +[Features](#features) · [Getting started](#getting-started) · [Auto-load](#auto-load) · [Acknowledgements](#acknowledgements) + +
+ +## Features + +- God mode, infinite health and mana, auto potions, flight and movement controls. +- Predictive aimbot, auto attack, shot preview and kill aura. +- Item and buff browsers, infinite items, instant break and auto fishing. +- ESP, health bars, projectile paths and potion vision effects. +- Time, weather and event controls, plus saved waypoints with teleportation. + +Hotkeys are rebindable. Settings, ESP colors and window positions save automatically; waypoints are saved per world. + +Mostly intended for single player. Changes to NPCs and the world only affect your client in multiplayer. + +## Getting started + +To build from source, you'll need: + +- Terraria or tModLoader installed. The build script skips games missing from the configured paths; tModLoader is optional. +- The .NET 8 SDK, plus the .NET Framework 4.8 Developer Pack for Terraria builds. +- Visual Studio 2022 or newer with the C++ build tools. +- XNA Framework 4.0 for Terraria builds, installed with the game. + +Set `TerrariaDir` and `TModLoaderDir` in `Directory.Build.props` to your game folders. The build and auto-load scripts use these paths. Environment variables with the same names also work. + +1. Run `build.bat` from the project folder. The files go into `bin\`. +2. Start Terraria or tModLoader, then run `bin\TerraSharp.Injector.exe`. +3. Press `Insert` to open the menu. You can change this on the Keys tab. + +The same injector works with both games. Keep it beside the DLLs produced by the build. + +## Auto-load + +You can also have the menu load when the game starts. Close the game, run `autoload.bat` and choose Install. + +For Terraria, the script installs the DLL and adds its AppDomainManager entries to the existing game configuration. Uninstall removes those entries and the DLL while preserving other settings. Installation stops if another AppDomainManager is already configured. A game verify/update on Steam may remove the configuration; run the installer again if the menu stops loading. + +For tModLoader, the script prints a launch option to paste into Steam under **Properties > General > Launch Options**. Steam also uses `autoload.bat` as the Windows launcher: it sets `DOTNET_STARTUP_HOOKS`, preserves any existing hooks and runs Steam's game command. Keep the project folder in place while using this option. When updating from an earlier version, run Install again and replace the launch option. Remove the option to stop auto-loading. It does not appear in tModLoader's mod list. + +## Settings + +Settings save to `TerraSharp.json` in the game's save folder: + +- Terraria: `Documents\My Games\Terraria\TerraSharp.json` +- tModLoader: `Documents\My Games\Terraria\tModLoader\TerraSharp.json` + +For development with the game running, use `build-and-inject.bat`. It builds and reloads the menu, with logging to `%TEMP%\terrasharp.log`, and opens a live log viewer: gray for information, dark gray for debug messages, yellow for warnings, and red for errors. Repeated builds reuse the viewer. You can also open it with `build-and-inject.bat log`. Closing the viewer leaves the game running. + +## Acknowledgements + +- [glennf](https://www.unknowncheats.me/forum/members/5262305.html) for his [GVoid Terraria External Trainer](https://www.unknowncheats.me/forum/other-mmorpg-and-strategy/756815-terraria-external-v1-4-5-6-a.html). +- [MonoMod developers](https://github.com/MonoMod/MonoMod) for RuntimeDetour, which handles the runtime hooks. diff --git a/TerraSharp.slnx b/TerraSharp.slnx new file mode 100644 index 0000000..0fa05e7 --- /dev/null +++ b/TerraSharp.slnx @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/menu.png b/assets/menu.png new file mode 100644 index 0000000000000000000000000000000000000000..21ba2074802d919296b1eac56fc6e0f339c9e453 GIT binary patch literal 242662 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

kqa`a+NH=912L#b!|U20E{ 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 || (echo copy failed, close Terraria first & goto fail) +call :configure Install +if errorlevel 1 goto fail +echo installed. Start Terraria normally; run autoload.bat again to remove. + +:tmodloader +if not exist "%TModLoaderDir%\tModLoader.dll" goto done +echo. +echo tModLoader: add this to its Steam launch options (Properties ^> General ^> Launch Options): +echo. +powershell -NoProfile -NonInteractive -Command "[Console]::WriteLine(' cmd /d /s /c ' + [char]34 + [char]34 + $env:ROOT + 'autoload.bat' + [char]34 + ' %%command%%' + [char]34)" +echo. +if not exist "%HOOK%" echo (build the tModLoader DLL first: build.bat) +goto done + +:uninstall +echo. +if not defined TerrariaDir goto removeHook +call :configure Uninstall +if errorlevel 1 goto fail +del /q "%TerrariaDir%\TerraSharp.Vanilla.dll" 2>nul +if exist "%TerrariaDir%\TerraSharp.Vanilla.dll" (echo Terraria: TerraSharp.Vanilla.dll in use, close Terraria first & goto fail) +echo Terraria auto-load removed. +:removeHook +if defined TModLoaderDir echo tModLoader: remove the TerraSharp launcher from its Steam launch options. +goto done + +:fail +pause +exit /b 1 + +:done +pause +exit /b 0 + +:configure +set "TERRASHARP_CONFIG_MODE=%~1" +set "TERRASHARP_CONFIG_PATH=%TerrariaDir%\Terraria.exe.config" +set "TERRASHARP_CONFIG_VALIDATE=%~2" +powershell -NoProfile -NonInteractive -Command "$source = [IO.File]::ReadAllText($env:TERRASHARP_AUTOLOAD_SCRIPT); & ([scriptblock]::Create($source.Substring($source.LastIndexOf('# PowerShell')))) -Mode $env:TERRASHARP_CONFIG_MODE -ConfigPath $env:TERRASHARP_CONFIG_PATH -ValidateOnly:($env:TERRASHARP_CONFIG_VALIDATE -eq '1')" +exit /b %errorlevel% + +:launcher +if not exist "%HOOK%" (echo TerraSharp: "%HOOK%" not found, run build.bat first & exit /b 1) +if not defined DOTNET_STARTUP_HOOKS ( + set "DOTNET_STARTUP_HOOKS=%HOOK%" + goto launch +) +for %%h in ("%DOTNET_STARTUP_HOOKS:;=" "%") do if /i "%%~h"=="%HOOK%" goto launch +set "DOTNET_STARTUP_HOOKS=%DOTNET_STARTUP_HOOKS%;%HOOK%" + +:launch +%* +exit /b %errorlevel% + +# PowerShell +param( + [Parameter(Mandatory = $true)] + [ValidateSet('Install', 'Uninstall')] + [string]$Mode, + [Parameter(Mandatory = $true)] + [string]$ConfigPath, + [switch]$ValidateOnly +) + +$ErrorActionPreference = 'Stop' +$managerType = 'TerraSharp.LoaderDomainManager' +$assemblyName = 'TerraSharp.Vanilla' +$assemblyIdentity = 'TerraSharp.Vanilla, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' +$ConfigPath = [IO.Path]::GetFullPath($ConfigPath) +$document = New-Object Xml.XmlDocument +$document.PreserveWhitespace = $true +$document.XmlResolver = $null + +try { + if ([IO.File]::Exists($ConfigPath)) { + $readerSettings = New-Object Xml.XmlReaderSettings + $readerSettings.DtdProcessing = [Xml.DtdProcessing]::Prohibit + $reader = [Xml.XmlReader]::Create($ConfigPath, $readerSettings) + try { $document.Load($reader) } finally { $reader.Dispose() } + } elseif ($Mode -eq 'Uninstall') { + exit 0 + } else { + $document.LoadXml('' + "`r`n`r`n") + } + + if ($document.DocumentElement.Name -ne 'configuration' -or $document.DocumentElement.NamespaceURI) { + throw 'Expected a configuration root element.' + } + + $runtimeNodes = $document.SelectNodes('/configuration/runtime') + if ($runtimeNodes.Count -gt 1) { + throw 'The configuration contains multiple runtime sections.' + } + + $runtime = $document.SelectSingleNode('/configuration/runtime') + $typeNodes = @($document.SelectNodes('/configuration/runtime/appDomainManagerType')) + $assemblyNodes = @($document.SelectNodes('/configuration/runtime/appDomainManagerAssembly')) + + if ($Mode -eq 'Install') { + foreach ($typeNode in $typeNodes) { + if ($typeNode.GetAttribute('value') -cne $managerType) { + throw 'Another AppDomainManager is configured; its entries were left unchanged.' + } + } + foreach ($assemblyNode in $assemblyNodes) { + if ($assemblyNode.GetAttribute('value').Split(',')[0].Trim() -ine $assemblyName) { + throw 'Another AppDomainManager assembly is configured; its entries were left unchanged.' + } + } + if ($typeNodes.Count -gt 1 -or $assemblyNodes.Count -gt 1) { + throw 'The configuration contains duplicate AppDomainManager entries.' + } + if ($ValidateOnly) { + exit 0 + } + + $changed = $false + if ($null -eq $runtime) { + $runtime = $document.CreateElement('runtime') + [void]$document.DocumentElement.AppendChild($runtime) + $changed = $true + } + + foreach ($entry in @(@('appDomainManagerType', $managerType), @('appDomainManagerAssembly', $assemblyIdentity))) { + $element = $runtime.SelectSingleNode($entry[0]) + if ($null -eq $element) { + $element = $document.CreateElement($entry[0]) + [void]$runtime.AppendChild($element) + } + if ($element.GetAttribute('value') -cne $entry[1]) { + $element.SetAttribute('value', $entry[1]) + $changed = $true + } + } + } else { + $changed = $false + foreach ($typeNode in $typeNodes) { + if ($typeNode.GetAttribute('value') -ceq $managerType) { + [void]$runtime.RemoveChild($typeNode) + $changed = $true + } + } + foreach ($assemblyNode in $assemblyNodes) { + if ($assemblyNode.GetAttribute('value').Split(',')[0].Trim() -ieq $assemblyName) { + [void]$runtime.RemoveChild($assemblyNode) + $changed = $true + } + } + } + + if (-not $changed) { + exit 0 + } + + $temporaryPath = Join-Path ([IO.Path]::GetDirectoryName($ConfigPath)) ([Guid]::NewGuid().ToString('N') + '.tmp') + try { + $document.Save($temporaryPath) + if ([IO.File]::Exists($ConfigPath)) { + [IO.File]::Replace($temporaryPath, $ConfigPath, [NullString]::Value) + } else { + [IO.File]::Move($temporaryPath, $ConfigPath) + } + } finally { + if ([IO.File]::Exists($temporaryPath)) { + [IO.File]::Delete($temporaryPath) + } + } +} catch { + [Console]::Error.WriteLine('TerraSharp configuration: ' + $_.Exception.Message) + exit 1 +} diff --git a/build-and-inject.bat b/build-and-inject.bat new file mode 100644 index 0000000..f0babca --- /dev/null +++ b/build-and-inject.bat @@ -0,0 +1,73 @@ +@echo off +rem Builds into run\\ under a unique assembly name and injects it into whichever of Terraria or tModLoader is running: the game locks the injected DLL and binds simple-named assemblies by name only. +setlocal DisableDelayedExpansion +set "ROOT=%~dp0" +if /i "%~1"=="log" goto log + +set "STAMP=%DATE%%TIME%" +for %%c in ("/" ":" "." "," "-" " ") do call set "STAMP=%%STAMP:%%~c=%%" +set "OUT=%ROOT%run\%STAMP%" + +set "GamePathsLoaded=" +for /f "usebackq tokens=1,* delims==" %%a in (`powershell -NoProfile -NonInteractive -Command "$ErrorActionPreference = 'Stop'; try { $properties = [xml](Get-Content -LiteralPath ($env:ROOT + 'Directory.Build.props') -Raw); foreach ($name in 'TerrariaDir', 'TModLoaderDir') { $value = $properties.SelectSingleNode('/Project/PropertyGroup/' + $name).InnerText; Write-Output ($name + '=' + $value) }; 'GamePathsLoaded=1' } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }"`) do if not defined %%a set "%%a=%%b" +if not defined GamePathsLoaded exit /b 1 + +if not exist "%TerrariaDir%\Terraria.exe" (echo Terraria not found at the configured path, skipping its build & goto tmodloader) +echo Terraria: "%TerrariaDir%" +dotnet build "%ROOT%src\TerraSharp\TerraSharp.csproj" -c Release -f net48 -o "%OUT%" "-p:TerrariaDir=%TerrariaDir%" -p:AssemblyName=TerraSharp.Vanilla_%STAMP% -p:TerraSharpLogging=true || exit /b 1 + +:tmodloader +if not exist "%TModLoaderDir%\tModLoader.dll" (echo tModLoader not found at the configured path, skipping its build & goto inject) +echo tModLoader: "%TModLoaderDir%" +dotnet build "%ROOT%src\TerraSharp\TerraSharp.csproj" -c Release -f net8.0 -o "%OUT%" "-p:TModLoaderDir=%TModLoaderDir%" -p:AssemblyName=TerraSharp.tModLoader_%STAMP% -p:TerraSharpLogging=true || exit /b 1 + +:inject +if not exist "%TerrariaDir%\Terraria.exe" if not exist "%TModLoaderDir%\tModLoader.dll" (echo Set TerrariaDir or TModLoaderDir in Directory.Build.props to an installed game & exit /b 1) +call "%ROOT%build.bat" injector || exit /b 1 +start "TerraSharp log" "%ComSpec%" /d /c ""%~f0" log" +"%ROOT%bin\TerraSharp.Injector.exe" "%OUT%" +exit /b %errorlevel% + +:log +set "TERRASHARP_LOG_SCRIPT=%~f0" +powershell -NoProfile -Command "$source = Get-Content -LiteralPath $env:TERRASHARP_LOG_SCRIPT -Raw; & ([scriptblock]::Create(($source -split '(?m)^# TerraSharp log viewer\r?$', 2)[1]))" +exit /b %errorlevel% + +# TerraSharp log viewer +$ErrorActionPreference = 'Stop' +$createdNew = $false +$viewerMutex = [System.Threading.Mutex]::new($true, 'Local\TerraSharp.LogViewer', [ref]$createdNew) + +try { + if (-not $createdNew) { + return + } + + $Host.UI.RawUI.WindowTitle = 'TerraSharp log' + $logPath = Join-Path ([System.IO.Path]::GetTempPath()) 'terrasharp.log' + Write-Host "Following $logPath (Ctrl+C to stop)" -ForegroundColor Gray + while (-not (Test-Path -LiteralPath $logPath -PathType Leaf)) { + Start-Sleep -Milliseconds 200 + } + + $color = 'Gray' + Get-Content -LiteralPath $logPath -Encoding UTF8 -Tail 30 -Wait | ForEach-Object { + if ($_ -match '^\d{2}:\d{2}:\d{2}(?:\.\d{3})? (?:\[(DEBUG|INFO|WARNING|ERROR)\] )?') { + $color = switch ($Matches[1]) { + 'DEBUG' { 'DarkGray' } + 'INFO' { 'Gray' } + 'WARNING' { 'Yellow' } + 'ERROR' { 'Red' } + default { 'Gray' } + } + } + + Write-Host $_ -ForegroundColor $color + } +} +finally { + if ($createdNew) { + $viewerMutex.ReleaseMutex() + } + $viewerMutex.Dispose() +} diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..ba81bd6 --- /dev/null +++ b/build.bat @@ -0,0 +1,98 @@ +@echo off +rem Builds the configured game targets and the injector into bin\. +setlocal DisableDelayedExpansion +set "ROOT=%~dp0" +set "OUT=%ROOT%bin" +set "OBJ=%ROOT%artifacts\obj\native" +set "SRC=%ROOT%src\TerraSharp.Injector" +set "NATIVE_ARCH=" + +if /i "%~1"=="injector" goto native +set "GamePathsLoaded=" +for /f "usebackq tokens=1,* delims==" %%a in (`powershell -NoProfile -NonInteractive -Command "$ErrorActionPreference = 'Stop'; try { $properties = [xml](Get-Content -LiteralPath ($env:ROOT + 'Directory.Build.props') -Raw); foreach ($name in 'TerrariaDir', 'TModLoaderDir') { $value = $properties.SelectSingleNode('/Project/PropertyGroup/' + $name).InnerText; Write-Output ($name + '=' + $value) }; 'GamePathsLoaded=1' } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }"`) do if not defined %%a set "%%a=%%b" +if not defined GamePathsLoaded exit /b 1 + +if not exist "%TerrariaDir%\Terraria.exe" (echo Terraria not found at the configured path, skipping its build & goto tmodloader) +echo Terraria: "%TerrariaDir%" +dotnet build "%ROOT%src\TerraSharp\TerraSharp.csproj" -c Release -f net48 -o "%OUT%" "-p:TerrariaDir=%TerrariaDir%" || exit /b 1 + +:tmodloader +if not exist "%TModLoaderDir%\tModLoader.dll" (echo tModLoader not found at the configured path, skipping its build & goto native) +echo tModLoader: "%TModLoaderDir%" +dotnet build "%ROOT%src\TerraSharp\TerraSharp.csproj" -c Release -f net8.0 -o "%OUT%" "-p:TModLoaderDir=%TModLoaderDir%" || exit /b 1 + +:native +if /i not "%~1"=="injector" if not exist "%TerrariaDir%\Terraria.exe" if not exist "%TModLoaderDir%\tModLoader.dll" (echo Set TerrariaDir or TModLoaderDir in Directory.Build.props to an installed game & exit /b 1) +set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +for /f "usebackq delims=" %%i in (`call "%VSWHERE%" -latest -products * -property installationPath`) do set "VS=%%i" +if not defined VS (echo Visual Studio not found & exit /b 1) +if not exist "%OUT%" mkdir "%OUT%" +call :assemble || exit /b 1 +call :assembleX64 || exit /b 1 +call :embed || exit /b 1 +call :compile || exit /b 1 +del /q "%OUT%\TerraSharp.Bootstrap.x86.dll" "%OUT%\TerraSharp.Bootstrap.x64.dll" "%OUT%\TerraSharp.tModLoader.runtimeconfig.json" 2>nul +echo Built into %OUT% +exit /b 0 + +:assemble +call :check "%OBJ%\x86\bootstrap.obj" "%SRC%\bootstrap-x86.asm" +if errorlevel 2 exit /b 1 +if not errorlevel 1 (echo x86 bootstrap is up to date & exit /b 0) + +call :setup x86 || exit /b 1 +if not exist "%OBJ%\x86" mkdir "%OBJ%\x86" +ml /nologo /c /Fo"%OBJ%\x86\bootstrap.obj" "%SRC%\bootstrap-x86.asm" || (del /q "%OBJ%\x86\bootstrap.obj" 2>nul & exit /b 1) +exit /b 0 + +:assembleX64 +call :check "%OBJ%\x64\bootstrap.obj" "%SRC%\bootstrap-x64.asm" +if errorlevel 2 exit /b 1 +if not errorlevel 1 (echo x64 bootstrap is up to date & exit /b 0) + +call :setup x64 || exit /b 1 +if not exist "%OBJ%\x64" mkdir "%OBJ%\x64" +ml64 /nologo /c /Fo"%OBJ%\x64\bootstrap.obj" "%SRC%\bootstrap-x64.asm" || (del /q "%OBJ%\x64\bootstrap.obj" 2>nul & exit /b 1) +exit /b 0 + +:embed +call :check "%OBJ%\bootstrap.generated.h" "%OBJ%\x86\bootstrap.obj" "%OBJ%\x64\bootstrap.obj" "%SRC%\embed-bootstrap.ps1" +if errorlevel 2 exit /b 1 +if not errorlevel 1 (echo Embedded bootstrap header is up to date & exit /b 0) + +powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%SRC%\embed-bootstrap.ps1" -X86Object "%OBJ%\x86\bootstrap.obj" -X64Object "%OBJ%\x64\bootstrap.obj" -Output "%OBJ%\bootstrap.generated.h" || (del /q "%OBJ%\bootstrap.generated.h" 2>nul & exit /b 1) +exit /b 0 + +:compile +call :check "%OUT%\TerraSharp.Injector.exe" "%SRC%\injector.cpp" "%SRC%\bootstrap.h" "%OBJ%\bootstrap.generated.h" +if errorlevel 2 exit /b 1 +if not errorlevel 1 (echo Injector is up to date & exit /b 0) + +call :setup x64 || exit /b 1 +if not exist "%OBJ%\x64" mkdir "%OBJ%\x64" +set "FLAGS=/nologo /W4 /O2 /EHsc /std:c++17 /MT /DUNICODE /D_UNICODE /I"%OBJ%" /Fo"%OBJ%\x64/"" +cl %FLAGS% "%SRC%\injector.cpp" /Fe"%OUT%\TerraSharp.Injector.exe" /link mscoree.lib || (del /q "%OUT%\TerraSharp.Injector.exe" 2>nul & exit /b 1) +exit /b 0 + +:check +setlocal +set "BUILD_CHECK_OUTPUT=%~1" +set "BUILD_CHECK_INPUTS=%ROOT%build.bat" +shift + +:checkInput +if "%~1"=="" goto checkTimestamps +set "BUILD_CHECK_INPUTS=%BUILD_CHECK_INPUTS%|%~1" +shift +goto checkInput + +:checkTimestamps +rem Exit codes: 0 up to date, 1 rebuild, 2 check failed. +powershell -NoProfile -NonInteractive -Command "$ErrorActionPreference = 'Stop'; try { $inputFiles = @($env:BUILD_CHECK_INPUTS.Split('|') | ForEach-Object { Get-Item -LiteralPath $_ }); if (-not (Test-Path -LiteralPath $env:BUILD_CHECK_OUTPUT -PathType Leaf)) { exit 1 }; $outputTime = (Get-Item -LiteralPath $env:BUILD_CHECK_OUTPUT).LastWriteTimeUtc; foreach ($inputFile in $inputFiles) { if ($inputFile.LastWriteTimeUtc -gt $outputTime) { exit 1 } }; exit 0 } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 }" +endlocal & exit /b %errorlevel% + +:setup +if "%NATIVE_ARCH%"=="%~1" exit /b 0 +call "%VS%\VC\Auxiliary\Build\vcvarsall.bat" %~1 >nul 2>&1 || exit /b 1 +set "NATIVE_ARCH=%~1" +exit /b 0 diff --git a/src/TerraSharp.Injector/bootstrap-x64.asm b/src/TerraSharp.Injector/bootstrap-x64.asm new file mode 100644 index 0000000..240577c --- /dev/null +++ b/src/TerraSharp.Injector/bootstrap-x64.asm @@ -0,0 +1,93 @@ +option casemap:none + +DataStage equ 0 +DataResult equ 4 +DataManagedResult equ 8 +DataCompleted equ 12 +DataAssembly equ 16 +DataTypeName equ 24 +DataMethodName equ 32 +DataGetRuntimeDelegate equ 64 + +StageGetDelegates equ 7 +StageLoadAssembly equ 8 +StageGetEntryPoint equ 9 +StageInvoke equ 10 + +DelegateGetFunctionPointer equ 6 +DelegateLoadAssembly equ 7 + +LocalLoadAssembly equ 30h +LocalGetFunction equ 38h +LocalEntry equ 40h + +.code +Bootstrap proc frame + push rbx + .pushreg rbx + sub rsp, 50h + .allocstack 50h + .endprolog + + mov rbx, rcx + mov dword ptr [rbx + DataResult], 80004005h + mov dword ptr [rbx + DataManagedResult], -1 + mov dword ptr [rbx + DataCompleted], 0 + xor eax, eax + mov [rsp + LocalLoadAssembly], rax + mov [rsp + LocalGetFunction], rax + mov [rsp + LocalEntry], rax + + mov dword ptr [rbx + DataStage], StageGetDelegates + xor ecx, ecx + mov edx, DelegateLoadAssembly + lea r8, [rsp + LocalLoadAssembly] + call qword ptr [rbx + DataGetRuntimeDelegate] + mov [rbx + DataResult], eax + test eax, eax + js Complete + + xor ecx, ecx + mov edx, DelegateGetFunctionPointer + lea r8, [rsp + LocalGetFunction] + call qword ptr [rbx + DataGetRuntimeDelegate] + mov [rbx + DataResult], eax + test eax, eax + js Complete + + mov dword ptr [rbx + DataStage], StageLoadAssembly + mov rcx, [rbx + DataAssembly] + xor edx, edx + xor r8d, r8d + call qword ptr [rsp + LocalLoadAssembly] + mov [rbx + DataResult], eax + test eax, eax + js Complete + + mov dword ptr [rbx + DataStage], StageGetEntryPoint + mov rcx, [rbx + DataTypeName] + mov rdx, [rbx + DataMethodName] + xor r8d, r8d + xor r9d, r9d + mov qword ptr [rsp + 20h], 0 + lea rax, [rsp + LocalEntry] + mov [rsp + 28h], rax + call qword ptr [rsp + LocalGetFunction] + mov [rbx + DataResult], eax + test eax, eax + js Complete + + mov dword ptr [rbx + DataStage], StageInvoke + xor ecx, ecx + xor edx, edx + call qword ptr [rsp + LocalEntry] + mov [rbx + DataManagedResult], eax + +Complete: + mov dword ptr [rbx + DataCompleted], 1 + mov eax, [rbx + DataResult] + add rsp, 50h + pop rbx + ret +Bootstrap endp +end diff --git a/src/TerraSharp.Injector/bootstrap-x86.asm b/src/TerraSharp.Injector/bootstrap-x86.asm new file mode 100644 index 0000000..3622521 --- /dev/null +++ b/src/TerraSharp.Injector/bootstrap-x86.asm @@ -0,0 +1,134 @@ +.386 +.model flat +option casemap:none + +StageOffset EQU 0 +ResultOffset EQU 4 +ManagedResultOffset EQU 8 +CompletedOffset EQU 12 +AssemblyOffset EQU 16 +TypeOffset EQU 24 +MethodOffset EQU 32 +ArgumentOffset EQU 40 +VersionOffset EQU 48 +CreateInstanceOffset EQU 56 +ClsidMetaOffset EQU 72 +IidMetaOffset EQU 80 +IidInfoOffset EQU 88 +ClsidHostOffset EQU 96 +IidHostOffset EQU 104 + +CreateInstanceStage EQU 1 +GetRuntimeStage EQU 2 +GetInterfaceStage EQU 3 +StartStage EQU 4 +ExecuteStage EQU 5 + +; Vtable offsets come from metahost.h and mscoree.h in the .NET Framework SDK. +ReleaseOffset EQU 8 +GetRuntimeOffset EQU 12 +GetInterfaceOffset EQU 36 +StartOffset EQU 12 +ExecuteOffset EQU 44 + +.code +Bootstrap PROC + push ebp + mov ebp, esp + push esi + push edi + sub esp, 12 + mov edi, [ebp + 8] + xor eax, eax + mov [ebp - 12], eax + mov [ebp - 16], eax + mov [ebp - 20], eax + + mov DWORD PTR [edi + StageOffset], CreateInstanceStage + lea eax, [ebp - 12] + push eax + push DWORD PTR [edi + IidMetaOffset] + push DWORD PTR [edi + ClsidMetaOffset] + call DWORD PTR [edi + CreateInstanceOffset] + mov [edi + ResultOffset], eax + test eax, eax + js cleanup + + mov DWORD PTR [edi + StageOffset], GetRuntimeStage + mov esi, [ebp - 12] + lea eax, [ebp - 16] + push eax + push DWORD PTR [edi + IidInfoOffset] + push DWORD PTR [edi + VersionOffset] + push esi + mov eax, [esi] + call DWORD PTR [eax + GetRuntimeOffset] + mov [edi + ResultOffset], eax + test eax, eax + js cleanup + + mov DWORD PTR [edi + StageOffset], GetInterfaceStage + mov esi, [ebp - 16] + lea eax, [ebp - 20] + push eax + push DWORD PTR [edi + IidHostOffset] + push DWORD PTR [edi + ClsidHostOffset] + push esi + mov eax, [esi] + call DWORD PTR [eax + GetInterfaceOffset] + mov [edi + ResultOffset], eax + test eax, eax + js cleanup + + mov DWORD PTR [edi + StageOffset], StartStage + mov esi, [ebp - 20] + push esi + mov eax, [esi] + call DWORD PTR [eax + StartOffset] + mov [edi + ResultOffset], eax + test eax, eax + js cleanup + + mov DWORD PTR [edi + StageOffset], ExecuteStage + lea eax, [edi + ManagedResultOffset] + push eax + push DWORD PTR [edi + ArgumentOffset] + push DWORD PTR [edi + MethodOffset] + push DWORD PTR [edi + TypeOffset] + push DWORD PTR [edi + AssemblyOffset] + push esi + mov eax, [esi] + call DWORD PTR [eax + ExecuteOffset] + mov [edi + ResultOffset], eax + +cleanup: + mov esi, [ebp - 20] + test esi, esi + jz releaseInfo + push esi + mov eax, [esi] + call DWORD PTR [eax + ReleaseOffset] +releaseInfo: + mov esi, [ebp - 16] + test esi, esi + jz releaseMeta + push esi + mov eax, [esi] + call DWORD PTR [eax + ReleaseOffset] +releaseMeta: + mov esi, [ebp - 12] + test esi, esi + jz done + push esi + mov eax, [esi] + call DWORD PTR [eax + ReleaseOffset] +done: + mov DWORD PTR [edi + CompletedOffset], 1 + mov eax, [edi + ResultOffset] + add esp, 12 + pop edi + pop esi + pop ebp + ret 4 +Bootstrap ENDP +END diff --git a/src/TerraSharp.Injector/bootstrap.h b/src/TerraSharp.Injector/bootstrap.h new file mode 100644 index 0000000..f7c72ab --- /dev/null +++ b/src/TerraSharp.Injector/bootstrap.h @@ -0,0 +1,55 @@ +#pragma once +#define NOMINMAX +#include +#include + +enum class BootstrapStage : DWORD +{ + Pending, + CreateInstance, + GetRuntime, + GetInterface, + Start, + Execute, + RegisterUnwind, + GetDelegates, + LoadAssembly, + GetEntryPoint, + Invoke, + RemoveUnwind, +}; + +// Layout and stages match bootstrap-x86.asm and bootstrap-x64.asm; pointer slots are always 64 bits. +struct BootstrapData +{ + BootstrapStage stage; + HRESULT result; + INT32 managedResult; + BOOL completed; + UINT64 assembly; + UINT64 typeName; + UINT64 methodName; + UINT64 argument; + UINT64 runtimeVersion; + UINT64 clrCreateInstance; + UINT64 getRuntimeDelegate; + UINT64 clsidMetaHost; + UINT64 iidMetaHost; + UINT64 iidRuntimeInfo; + UINT64 clsidRuntimeHost; + UINT64 iidRuntimeHost; +}; + +static_assert(offsetof(BootstrapData, assembly) == 16); +static_assert(offsetof(BootstrapData, typeName) == 24); +static_assert(offsetof(BootstrapData, methodName) == 32); +static_assert(offsetof(BootstrapData, argument) == 40); +static_assert(offsetof(BootstrapData, runtimeVersion) == 48); +static_assert(offsetof(BootstrapData, clrCreateInstance) == 56); +static_assert(offsetof(BootstrapData, getRuntimeDelegate) == 64); +static_assert(offsetof(BootstrapData, clsidMetaHost) == 72); +static_assert(offsetof(BootstrapData, iidMetaHost) == 80); +static_assert(offsetof(BootstrapData, iidRuntimeInfo) == 88); +static_assert(offsetof(BootstrapData, clsidRuntimeHost) == 96); +static_assert(offsetof(BootstrapData, iidRuntimeHost) == 104); +static_assert(sizeof(BootstrapData) == 112); diff --git a/src/TerraSharp.Injector/embed-bootstrap.ps1 b/src/TerraSharp.Injector/embed-bootstrap.ps1 new file mode 100644 index 0000000..34b8c72 --- /dev/null +++ b/src/TerraSharp.Injector/embed-bootstrap.ps1 @@ -0,0 +1,155 @@ +param( + [Parameter(Mandatory = $true)][string]$X86Object, + [Parameter(Mandatory = $true)][string]$X64Object, + [Parameter(Mandatory = $true)][string]$Output +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Assert-Range([byte[]]$Bytes, [long]$Offset, [long]$Size) { + if ($Offset -lt 0 -or $Size -lt 0 -or $Offset -gt $Bytes.Length -or $Size -gt $Bytes.Length - $Offset) { + throw 'COFF data extends beyond the object file.' + } +} + +function Read-UInt16([byte[]]$Bytes, [long]$Offset) { + Assert-Range $Bytes $Offset 2 + return [BitConverter]::ToUInt16($Bytes, [int]$Offset) +} + +function Read-UInt32([byte[]]$Bytes, [long]$Offset) { + Assert-Range $Bytes $Offset 4 + return [BitConverter]::ToUInt32($Bytes, [int]$Offset) +} + +function Read-CoffObject([string]$Path, [uint16]$Machine) { + $bytes = [IO.File]::ReadAllBytes([IO.Path]::GetFullPath($Path)) + Assert-Range $bytes 0 20 + if ((Read-UInt16 $bytes 0) -ne $Machine -or (Read-UInt16 $bytes 16) -ne 0) { + throw "Unexpected COFF object format: $Path" + } + $sectionCount = Read-UInt16 $bytes 2 + $symbolOffset = Read-UInt32 $bytes 8 + $symbolCount = Read-UInt32 $bytes 12 + $sectionTableEnd = 20 + [long]$sectionCount * 40 + Assert-Range $bytes 20 ([long]$sectionCount * 40) + $sections = @() + for ($index = 0; $index -lt $sectionCount; $index++) { + $offset = 20 + $index * 40 + $name = [Text.Encoding]::ASCII.GetString($bytes, $offset, 8).TrimEnd([char]0) + if ($name.StartsWith('/')) { + $stringTableOffset = [long]$symbolOffset + [long]$symbolCount * 18 + $stringTableSize = Read-UInt32 $bytes $stringTableOffset + Assert-Range $bytes $stringTableOffset $stringTableSize + $nameOffset = [uint32]0 + if (-not [uint32]::TryParse($name.Substring(1), [ref]$nameOffset) -or $nameOffset -lt 4 -or $nameOffset -ge $stringTableSize) { throw "Invalid COFF section name: $Path" } + $nameStart = $stringTableOffset + $nameOffset + $nameEnd = $nameStart + while ($nameEnd -lt $stringTableOffset + $stringTableSize -and $bytes[$nameEnd] -ne 0) { $nameEnd++ } + if ($nameEnd -eq $stringTableOffset + $stringTableSize) { throw "Unterminated COFF section name: $Path" } + $name = [Text.Encoding]::ASCII.GetString($bytes, [int]$nameStart, [int]($nameEnd - $nameStart)) + } + $size = Read-UInt32 $bytes ($offset + 16) + $dataOffset = Read-UInt32 $bytes ($offset + 20) + $relocationOffset = Read-UInt32 $bytes ($offset + 24) + $relocationCount = Read-UInt16 $bytes ($offset + 32) + $characteristics = Read-UInt32 $bytes ($offset + 36) + if ($relocationCount -eq 0xffff -or ($characteristics -band 0x01000000) -ne 0) { + throw "COFF relocation overflow is unsupported: $Path" + } + if ($size -gt 0) { + Assert-Range $bytes $dataOffset $size + if ($dataOffset -lt $sectionTableEnd) { throw "Invalid section data offset: $Path" } + } + if ($relocationCount -gt 0) { + Assert-Range $bytes $relocationOffset ([long]$relocationCount * 10) + if ($relocationOffset -lt $sectionTableEnd) { throw "Invalid relocation offset: $Path" } + } + if (($name -in '.text', '.xdata' -or $name.StartsWith('.text$', [StringComparison]::Ordinal)) -and $relocationCount -ne 0) { + throw "$name must be relocation-free: $Path" + } + $sections += [pscustomobject]@{ Name = $name; Index = $index + 1; Size = $size; DataOffset = $dataOffset; RelocationOffset = $relocationOffset; RelocationCount = $relocationCount } + } + $symbols = @{} + if ($symbolCount -gt 0) { + Assert-Range $bytes $symbolOffset ([long]$symbolCount * 18) + if ($symbolOffset -lt $sectionTableEnd) { throw "Invalid symbol table offset: $Path" } + for ($index = 0; $index -lt $symbolCount; $index++) { + $offset = $symbolOffset + [long]$index * 18 + $auxiliaryCount = $bytes[$offset + 17] + if ([long]$index + $auxiliaryCount -ge $symbolCount) { throw "Invalid auxiliary symbol count: $Path" } + $symbols[$index] = [pscustomobject]@{ Value = Read-UInt32 $bytes ($offset + 8); Section = [BitConverter]::ToInt16($bytes, [int]($offset + 12)) } + $index += $auxiliaryCount + } + } + return [pscustomobject]@{ Path = $Path; Bytes = $bytes; Sections = $sections; Symbols = $symbols } +} + +function Get-Section($Object, [string]$Name) { + $matches = @($Object.Sections | Where-Object { $_.Size -gt 0 -and ($_.Name -eq $Name -or ($Name -eq '.text' -and $_.Name.StartsWith('.text$', [StringComparison]::Ordinal))) }) + if ($matches.Count -ne 1) { + throw "Expected one nonempty $Name section: $($Object.Path)" + } + return $matches[0] +} + +function Get-SectionBytes($Object, $Section) { + $bytes = New-Object byte[] $Section.Size + [Array]::Copy($Object.Bytes, [int]$Section.DataOffset, $bytes, 0, [int]$Section.Size) + return ,$bytes +} + +function Resolve-UnwindReference($Object, $Section, [int]$Offset, [int]$ExpectedSection) { + $relocations = @() + for ($index = 0; $index -lt $Section.RelocationCount; $index++) { + $position = $Section.RelocationOffset + $index * 10 + if ((Read-UInt32 $Object.Bytes $position) -eq $Offset) { $relocations += $position } + } + if ($relocations.Count -ne 1) { throw 'Each RUNTIME_FUNCTION field must have exactly one relocation.' } + $position = $relocations[0] + if ((Read-UInt16 $Object.Bytes ($position + 8)) -ne 3) { throw 'RUNTIME_FUNCTION requires AMD64_ADDR32NB relocations.' } + $symbolIndex = Read-UInt32 $Object.Bytes ($position + 4) + if (-not $Object.Symbols.ContainsKey([int]$symbolIndex)) { throw 'RUNTIME_FUNCTION references an invalid or auxiliary symbol.' } + $symbol = $Object.Symbols[[int]$symbolIndex] + if ($symbol.Section -ne $ExpectedSection) { throw 'RUNTIME_FUNCTION references the wrong section.' } + return [long]$symbol.Value + (Read-UInt32 $Object.Bytes ($Section.DataOffset + $Offset)) +} + +function Append-ByteArray([Text.StringBuilder]$Builder, [string]$Name, [byte[]]$Bytes) { + [void]$Builder.AppendLine("inline constexpr unsigned char $Name[] =") + [void]$Builder.AppendLine('{') + for ($index = 0; $index -lt $Bytes.Length; $index += 16) { + $values = for ($position = $index; $position -lt [Math]::Min($index + 16, $Bytes.Length); $position++) { '0x{0:X2}' -f $Bytes[$position] } + [void]$Builder.AppendLine(' ' + ($values -join ', ') + ',') + } + [void]$Builder.AppendLine('};') + [void]$Builder.AppendLine() +} + +$x86 = Read-CoffObject $X86Object 0x014c +$x64 = Read-CoffObject $X64Object 0x8664 +$x86Code = Get-SectionBytes $x86 (Get-Section $x86 '.text') +$x64Text = Get-Section $x64 '.text' +$x64Unwind = Get-Section $x64 '.xdata' +$x64Functions = Get-Section $x64 '.pdata' +if ($x64Functions.Size -ne 12 -or $x64Functions.RelocationCount -ne 3) { throw 'The x64 bootstrap must have exactly one RUNTIME_FUNCTION.' } +$begin = Resolve-UnwindReference $x64 $x64Functions 0 $x64Text.Index +$end = Resolve-UnwindReference $x64 $x64Functions 4 $x64Text.Index +$unwindData = Resolve-UnwindReference $x64 $x64Functions 8 $x64Unwind.Index +if ($begin -ne 0 -or $end -ne $x64Text.Size -or $unwindData -ne 0) { throw 'The x64 frame must cover all code and reference the first UNWIND_INFO.' } +$x64Code = Get-SectionBytes $x64 $x64Text +$x64UnwindBytes = Get-SectionBytes $x64 $x64Unwind +if ($x64UnwindBytes.Length -lt 4 -or $x64UnwindBytes[0] -ne 1 -or ((4 + 2 * $x64UnwindBytes[2] + 3) -band -4) -gt $x64UnwindBytes.Length) { throw 'Unsupported or truncated x64 UNWIND_INFO.' } +$header = New-Object Text.StringBuilder +[void]$header.AppendLine('#pragma once') +[void]$header.AppendLine() +Append-ByteArray $header 'BootstrapX86' $x86Code +Append-ByteArray $header 'BootstrapX64' $x64Code +Append-ByteArray $header 'BootstrapX64Unwind' $x64UnwindBytes +[void]$header.AppendLine('inline constexpr DWORD BootstrapX64Begin = 0x{0:X8};' -f $begin) +[void]$header.AppendLine('inline constexpr DWORD BootstrapX64End = 0x{0:X8};' -f $end) +[void]$header.AppendLine('inline constexpr DWORD BootstrapX64UnwindData = 0x{0:X8};' -f $unwindData) +$outputPath = [IO.Path]::GetFullPath($Output) +[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($outputPath)) +[IO.File]::WriteAllText($outputPath, $header.ToString(), [Text.Encoding]::ASCII) diff --git a/src/TerraSharp.Injector/injector.cpp b/src/TerraSharp.Injector/injector.cpp new file mode 100644 index 0000000..c1a8675 --- /dev/null +++ b/src/TerraSharp.Injector/injector.cpp @@ -0,0 +1,574 @@ +// Usage: TerraSharp.Injector.exe [TerraSharp..dll | build directory]; injects into Terraria (x86, CLR v4) or tModLoader (x64, .NET 8). +#include "bootstrap.h" +#include "bootstrap.generated.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef _M_X64 +#error The injector must be built for x64. +#endif + +enum class Game { Terraria, TModLoader }; + +struct Target +{ + Game game; + const wchar_t* name; + const wchar_t* assembly; + bool x86; +}; + +static const Target Targets[] = +{ + { Game::Terraria, L"Terraria", L"TerraSharp.Vanilla", true }, + { Game::TModLoader, L"tModLoader", L"TerraSharp.tModLoader", false }, +}; + +struct Handle +{ + HANDLE value; + explicit Handle(HANDLE handle) : value(handle) {} + Handle(const Handle&) = delete; + Handle& operator=(const Handle&) = delete; + ~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); } +}; + +struct RemoteMemory +{ + HANDLE process; + void* address; + RemoteMemory(HANDLE target, SIZE_T size, bool x86 = false) : process(target), address(VirtualAllocEx(target, nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) + { + if (address && x86 && reinterpret_cast(address) + size > (UINT64{ 1 } << 32)) + { + VirtualFreeEx(process, address, 0, MEM_RELEASE); + address = nullptr; + SetLastError(ERROR_INVALID_ADDRESS); + } + } + RemoteMemory(const RemoteMemory&) = delete; + RemoteMemory& operator=(const RemoteMemory&) = delete; + ~RemoteMemory() { if (address) VirtualFreeEx(process, address, 0, MEM_RELEASE); } +}; + +static bool ContainsNoCase(std::wstring text, std::wstring part) +{ + for (wchar_t& character : text) + character = towlower(character); + for (wchar_t& character : part) + character = towlower(character); + return text.find(part) != std::wstring::npos; +} + +static std::wstring CommandLine(DWORD processId) +{ + using QueryFunction = NTSTATUS(NTAPI*)(HANDLE, ULONG, PVOID, ULONG, PULONG); + static auto query = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationProcess")); + const ULONG ProcessCommandLineInformation = 60; + Handle process(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId)); + if (!process.value || !query) + return {}; + ULONG size = 0; + query(process.value, ProcessCommandLineInformation, nullptr, 0, &size); + if (!size) + return {}; + std::vector buffer(size); + if (query(process.value, ProcessCommandLineInformation, buffer.data(), size, &size) < 0) + return {}; + auto text = reinterpret_cast(buffer.data()); + return std::wstring(text->Buffer, text->Length / sizeof(wchar_t)); +} + +static DWORD FindProcess(Game game) +{ + Handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)); + if (snapshot.value == INVALID_HANDLE_VALUE) + return 0; + PROCESSENTRY32W entry{ sizeof(entry) }; + for (BOOL hasProcess = Process32FirstW(snapshot.value, &entry); hasProcess; hasProcess = Process32NextW(snapshot.value, &entry)) + { + if (game == Game::Terraria && _wcsicmp(entry.szExeFile, L"Terraria.exe") == 0) + return entry.th32ProcessID; + // tModLoader runs as "dotnet tModLoader.dll"; its Steam client helper and dedicated server use the same command. + if (game == Game::TModLoader && _wcsicmp(entry.szExeFile, L"dotnet.exe") == 0) + { + std::wstring command = CommandLine(entry.th32ProcessID); + if (ContainsNoCase(command, L"tModLoader.dll") && !ContainsNoCase(command, L"-server") && !ContainsNoCase(command, L"-terrariasteamclient")) + return entry.th32ProcessID; + } + } + return 0; +} + +static bool ReadAll(const std::wstring& path, std::vector& bytes) +{ + std::ifstream file(path, std::ios::binary); + if (!file) + return false; + bytes.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + return true; +} + +struct PeImage +{ + std::vector bytes; + const IMAGE_NT_HEADERS32* headers = nullptr; + + bool Load(const std::wstring& path) + { + if (!ReadAll(path, bytes) || bytes.size() < sizeof(IMAGE_DOS_HEADER)) + return false; + auto dosHeader = reinterpret_cast(bytes.data()); + if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE || dosHeader->e_lfanew <= 0 || static_cast(dosHeader->e_lfanew) + sizeof(IMAGE_NT_HEADERS64) > bytes.size()) + return false; + headers = reinterpret_cast(bytes.data() + dosHeader->e_lfanew); + return headers->Signature == IMAGE_NT_SIGNATURE; + } + + WORD Machine() const { return headers->FileHeader.Machine; } + + const IMAGE_DATA_DIRECTORY& Directory(int index) const + { + if (headers->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) + return reinterpret_cast(headers)->OptionalHeader.DataDirectory[index]; + return headers->OptionalHeader.DataDirectory[index]; + } + + const unsigned char* At(DWORD relativeAddress, DWORD size) const + { + auto section = IMAGE_FIRST_SECTION(headers); + for (WORD i = 0; i < headers->FileHeader.NumberOfSections; i++, section++) + { + DWORD extent = std::max(section->Misc.VirtualSize, section->SizeOfRawData); + if (relativeAddress >= section->VirtualAddress && relativeAddress - section->VirtualAddress + size <= extent) + { + size_t offset = static_cast(section->PointerToRawData) + (relativeAddress - section->VirtualAddress); + return offset + size <= bytes.size() ? bytes.data() + offset : nullptr; + } + } + return nullptr; + } + + // Forwarded exports have an RVA inside the export directory; none of the names used here are forwarded. + DWORD Export(const char* name) const + { + const IMAGE_DATA_DIRECTORY& directory = Directory(IMAGE_DIRECTORY_ENTRY_EXPORT); + auto exports = reinterpret_cast(At(directory.VirtualAddress, sizeof(IMAGE_EXPORT_DIRECTORY))); + if (!exports) + return 0; + auto names = reinterpret_cast(At(exports->AddressOfNames, exports->NumberOfNames * sizeof(DWORD))); + auto ordinals = reinterpret_cast(At(exports->AddressOfNameOrdinals, exports->NumberOfNames * sizeof(WORD))); + auto functions = reinterpret_cast(At(exports->AddressOfFunctions, exports->NumberOfFunctions * sizeof(DWORD))); + if (!names || !ordinals || !functions) + return 0; + for (DWORD i = 0; i < exports->NumberOfNames; i++) + { + auto exportName = reinterpret_cast(At(names[i], 1)); + if (!exportName || strcmp(exportName, name) != 0 || ordinals[i] >= exports->NumberOfFunctions) + continue; + DWORD relativeAddress = functions[ordinals[i]]; + bool forwarded = relativeAddress >= directory.VirtualAddress && relativeAddress < directory.VirtualAddress + directory.Size; + return forwarded ? 0 : relativeAddress; + } + return 0; + } +}; + +struct ModuleInfo +{ + unsigned char* address = nullptr; + std::wstring path; +}; + +static std::wstring ModuleFilePath(std::wstring path, bool x86) +{ + if (!x86) + return path; + wchar_t system[MAX_PATH]; + UINT systemLength = GetSystemDirectoryW(system, MAX_PATH); + if (!systemLength || systemLength >= MAX_PATH || path.size() <= systemLength || path[systemLength] != L'\\' || _wcsnicmp(path.c_str(), system, systemLength) != 0) + return path; + // WOW64 module paths can retain System32 even when the mapped DLL comes from SysWOW64. + wchar_t wow64[MAX_PATH]; + UINT wow64Length = GetSystemWow64DirectoryW(wow64, MAX_PATH); + return wow64Length && wow64Length < MAX_PATH ? std::wstring(wow64, wow64Length) + path.substr(systemLength) : path; +} + +static ModuleInfo RemoteModule(HANDLE process, bool x86, const wchar_t* name) +{ + std::vector modules(1024); + DWORD needed = 0; + while (true) + { + if (!EnumProcessModulesEx(process, modules.data(), static_cast(modules.size() * sizeof(HMODULE)), &needed, x86 ? LIST_MODULES_32BIT : LIST_MODULES_64BIT)) + return {}; + if (needed <= modules.size() * sizeof(HMODULE)) + break; + modules.resize(needed / sizeof(HMODULE)); + } + for (DWORD i = 0; i < needed / sizeof(HMODULE); i++) + { + wchar_t base[MAX_PATH]; + if (GetModuleBaseNameW(process, modules[i], base, MAX_PATH) && _wcsicmp(base, name) == 0) + { + std::vector path(32768); + DWORD length = GetModuleFileNameExW(process, modules[i], path.data(), static_cast(path.size())); + if (length && length < path.size()) + return { reinterpret_cast(modules[i]), ModuleFilePath(std::wstring(path.data(), length), x86) }; + return {}; + } + } + return {}; +} + +static void* RemoteExport(HANDLE process, bool x86, const wchar_t* moduleName, const char* name) +{ + ModuleInfo module = RemoteModule(process, x86, moduleName); + PeImage image; + if (!module.address) + return nullptr; + if (!image.Load(module.path) || image.Machine() != (x86 ? IMAGE_FILE_MACHINE_I386 : IMAGE_FILE_MACHINE_AMD64)) + { + wprintf(L"cannot read an %s module image: %s\n", x86 ? L"x86" : L"x64", module.path.c_str()); + return nullptr; + } + DWORD relativeAddress = image.Export(name); + return relativeAddress ? module.address + relativeAddress : nullptr; +} + +enum class ThreadResult { NotStarted, Completed, Uncertain }; + +static ThreadResult RemoteCall(HANDLE process, void* function, void* argument, DWORD& exitCode) +{ + Handle thread(CreateRemoteThread(process, nullptr, 0, reinterpret_cast(function), argument, 0, nullptr)); + if (!thread.value) + { + wprintf(L"CreateRemoteThread failed: %lu\n", GetLastError()); + return ThreadResult::NotStarted; + } + if (WaitForSingleObject(thread.value, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeThread(thread.value, &exitCode)) + { + wprintf(L"waiting for the remote thread failed: %lu\n", GetLastError()); + return ThreadResult::Uncertain; + } + return ThreadResult::Completed; +} + +static const wchar_t* StageName(BootstrapStage stage) +{ + switch (stage) + { + case BootstrapStage::CreateInstance: return L"CLRCreateInstance"; + case BootstrapStage::GetRuntime: return L"GetRuntime"; + case BootstrapStage::GetInterface: return L"GetInterface"; + case BootstrapStage::Start: return L"Start"; + case BootstrapStage::Execute: return L"ExecuteInDefaultAppDomain"; + case BootstrapStage::RegisterUnwind: return L"RtlAddFunctionTable"; + case BootstrapStage::GetDelegates: return L"hostfxr_get_runtime_delegate"; + case BootstrapStage::LoadAssembly: return L"load_assembly"; + case BootstrapStage::GetEntryPoint: return L"get_function_pointer"; + case BootstrapStage::Invoke: return L"TerraSharp.Entry.Init"; + case BootstrapStage::RemoveUnwind: return L"RtlDeleteFunctionTable"; + default: return L"bootstrap"; + } +} + +#pragma pack(push, 1) +// Tail-calling RtlAddFunctionTable needs no unwind entry of its own. +struct UnwindRegistration +{ + BYTE loadTable[2]{ 0x48, 0xB9 }; + UINT64 table; + BYTE loadCount{ 0xBA }; + DWORD count{ 1 }; + BYTE loadBase[2]{ 0x49, 0xB8 }; + UINT64 base; + BYTE loadFunction[2]{ 0x48, 0xB8 }; + UINT64 function; + BYTE tailCall[2]{ 0xFF, 0xE0 }; +}; +#pragma pack(pop) +static_assert(sizeof(UnwindRegistration) == 37); + +static int Inject(HANDLE process, DWORD processId, const Target& target, const std::wstring& assembly) +{ + BOOL wow64 = FALSE; + if (!IsWow64Process(process, &wow64)) + { + wprintf(L"IsWow64Process failed: %lu\n", GetLastError()); + return 4; + } + if ((wow64 != FALSE) != target.x86) + { + wprintf(L"%s must be an %s process\n", target.name, target.x86 ? L"x86" : L"x64"); + return 4; + } + BootstrapData data{}; + data.result = E_PENDING; + data.managedResult = -1; + void* addFunctionTable = nullptr; + void* deleteFunctionTable = nullptr; + if (target.x86) + { + data.clrCreateInstance = reinterpret_cast(RemoteExport(process, true, L"mscoree.dll", "CLRCreateInstance")); + if (!data.clrCreateInstance) + { + wprintf(L"CLRCreateInstance is unavailable in target; wait for Terraria to finish starting\n"); + return 4; + } + } + else + { + data.getRuntimeDelegate = reinterpret_cast(RemoteExport(process, false, L"hostfxr.dll", "hostfxr_get_runtime_delegate")); + addFunctionTable = RemoteExport(process, false, L"ntdll.dll", "RtlAddFunctionTable"); + deleteFunctionTable = RemoteExport(process, false, L"ntdll.dll", "RtlDeleteFunctionTable"); + if (!data.getRuntimeDelegate || !addFunctionTable || !deleteFunctionTable) + { + wprintf(L"hostfxr or unwind APIs are unavailable in target; wait for tModLoader to finish starting\n"); + return 4; + } + } + + std::wstring type = L"TerraSharp.Entry"; + if (!target.x86) + { + size_t start = assembly.find_last_of(L"\\/") + 1; + type += L", " + assembly.substr(start, assembly.size() - start - 4); + } + const std::wstring version = L"v4.0.30319", method = L"Init", argument; + SIZE_T dataSize = sizeof(BootstrapData) + (assembly.size() + type.size() + version.size() + method.size() + argument.size() + 5) * sizeof(wchar_t) + 5 * sizeof(GUID) + 4; + SIZE_T codeSize = target.x86 ? sizeof(BootstrapX86) : sizeof(BootstrapX64); + SIZE_T unwindOffset = (codeSize + 3) & ~SIZE_T{ 3 }; + SIZE_T tableOffset = (unwindOffset + sizeof(BootstrapX64Unwind) + 3) & ~SIZE_T{ 3 }; + SIZE_T registrationOffset = tableOffset + sizeof(RUNTIME_FUNCTION); + SIZE_T allocationSize = target.x86 ? codeSize : registrationOffset + sizeof(UnwindRegistration); + RemoteMemory remoteData(process, dataSize, target.x86); + RemoteMemory remoteCode(process, allocationSize, target.x86); + if (!remoteData.address || !remoteCode.address) + { + wprintf(L"remote allocation failed: %lu\n", GetLastError()); + return 5; + } + std::vector payload(sizeof(BootstrapData)); + auto append = [&](const void* bytes, SIZE_T size) + { + UINT64 address = reinterpret_cast(remoteData.address) + payload.size(); + auto begin = static_cast(bytes); + payload.insert(payload.end(), begin, begin + size); + return address; + }; + auto text = [&](const std::wstring& value) { return append(value.c_str(), (value.size() + 1) * sizeof(wchar_t)); }; + data.assembly = text(assembly); + data.typeName = text(type); + data.methodName = text(method); + data.argument = text(argument); + data.runtimeVersion = text(version); + payload.resize((payload.size() + 3) & ~SIZE_T{ 3 }); + const GUID guids[]{ CLSID_CLRMetaHost, __uuidof(ICLRMetaHost), __uuidof(ICLRRuntimeInfo), CLSID_CLRRuntimeHost, __uuidof(ICLRRuntimeHost) }; + data.clsidMetaHost = append(&guids[0], sizeof(GUID)); + data.iidMetaHost = append(&guids[1], sizeof(GUID)); + data.iidRuntimeInfo = append(&guids[2], sizeof(GUID)); + data.clsidRuntimeHost = append(&guids[3], sizeof(GUID)); + data.iidRuntimeHost = append(&guids[4], sizeof(GUID)); + memcpy(payload.data(), &data, sizeof(data)); + + std::vector code(allocationSize); + memcpy(code.data(), target.x86 ? BootstrapX86 : BootstrapX64, codeSize); + auto codeBase = static_cast(remoteCode.address); + void* functionTable = codeBase + tableOffset; + if (!target.x86) + { + memcpy(code.data() + unwindOffset, BootstrapX64Unwind, sizeof(BootstrapX64Unwind)); + RUNTIME_FUNCTION function{ BootstrapX64Begin, BootstrapX64End, static_cast(unwindOffset) + BootstrapX64UnwindData }; + memcpy(code.data() + tableOffset, &function, sizeof(function)); + UnwindRegistration registration{}; + registration.table = reinterpret_cast(functionTable); + registration.base = reinterpret_cast(codeBase); + registration.function = reinterpret_cast(addFunctionTable); + memcpy(code.data() + registrationOffset, ®istration, sizeof(registration)); + } + if (!WriteProcessMemory(process, remoteData.address, payload.data(), payload.size(), nullptr) || !WriteProcessMemory(process, remoteCode.address, code.data(), code.size(), nullptr)) + { + wprintf(L"remote write failed: %lu\n", GetLastError()); + return 5; + } + + DWORD oldProtect; + if (!VirtualProtectEx(process, remoteCode.address, allocationSize, PAGE_EXECUTE_READ, &oldProtect) || !FlushInstructionCache(process, remoteCode.address, allocationSize)) + { + wprintf(L"preparing bootstrap code failed: %lu\n", GetLastError()); + return 5; + } + + DWORD exitCode = 0; + if (!target.x86) + { + ThreadResult registered = RemoteCall(process, codeBase + registrationOffset, nullptr, exitCode); + if (registered != ThreadResult::Completed || !(exitCode & 0xFF)) + { + if (registered == ThreadResult::Uncertain) + remoteCode.address = remoteData.address = nullptr; + wprintf(L"RtlAddFunctionTable failed\n"); + return 6; + } + } + ThreadResult ran = RemoteCall(process, codeBase + (target.x86 ? 0 : BootstrapX64Begin), remoteData.address, exitCode); + if (ran == ThreadResult::Uncertain) + { + // A running thread or registered unwind table can still reference these allocations. + remoteCode.address = remoteData.address = nullptr; + return 6; + } + bool read = ran == ThreadResult::Completed && ReadProcessMemory(process, remoteData.address, &data, sizeof(data), nullptr); + DWORD readError = read ? ERROR_SUCCESS : GetLastError(); + if (!target.x86) + { + DWORD removed = 0; + if (RemoteCall(process, deleteFunctionTable, functionTable, removed) != ThreadResult::Completed || !(removed & 0xFF)) + { + remoteCode.address = remoteData.address = nullptr; + wprintf(L"RtlDeleteFunctionTable failed; keeping the remote bootstrap allocated\n"); + return 6; + } + } + if (ran == ThreadResult::NotStarted) + return 6; + if (!read) + { + wprintf(L"reading bootstrap result failed: %lu\n", readError); + return 7; + } + if (!data.completed) + { + wprintf(L"bootstrap stopped during %s (exit=0x%08lX)\n", StageName(data.stage), exitCode); + return 7; + } + if (FAILED(data.result)) + { + wprintf(L"%s failed (hr=0x%08lX)\n", StageName(data.stage), static_cast(data.result)); + return 7; + } + if (data.managedResult != 0) + { + wprintf(L"TerraSharp.Entry.Init failed (return=0x%08lX)\n", static_cast(data.managedResult)); + return 8; + } + wprintf(L"injected into %s pid %lu\n", target.name, processId); + return 0; +} + +static std::wstring FullPath(const wchar_t* path) +{ + DWORD length = GetFullPathNameW(path, 0, nullptr, nullptr); + if (!length) + return {}; + std::vector buffer(length); + DWORD written = GetFullPathNameW(path, length, buffer.data(), nullptr); + return written && written < length ? std::wstring(buffer.data(), written) : std::wstring(); +} + +static bool IsFile(const std::wstring& path) +{ + DWORD attributes = GetFileAttributesW(path.c_str()); + return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY); +} + +static std::wstring WithSlash(std::wstring path) +{ + if (!path.empty() && path.back() != L'\\' && path.back() != L'/') + path += L'\\'; + return path; +} + +// Matched by prefix: build-and-inject.bat suffixes the assembly and file name with a per-build stamp. +static std::wstring FindAssembly(const std::wstring& directory, const Target& target) +{ + WIN32_FIND_DATAW found; + HANDLE search = FindFirstFileW((directory + target.assembly + L"*.dll").c_str(), &found); + if (search == INVALID_HANDLE_VALUE) + return {}; + std::wstring result = directory + found.cFileName; + bool single = !FindNextFileW(search, &found); + FindClose(search); + return single ? result : std::wstring(); +} + +int wmain(int argc, wchar_t** argv) +{ + if (argc > 2) + { + wprintf(L"usage: TerraSharp.Injector.exe [TerraSharp..dll | build directory]\n"); + return 1; + } + std::vector self(32768); + DWORD selfLength = GetModuleFileNameW(nullptr, self.data(), static_cast(self.size())); + std::wstring exeDirectory = selfLength && selfLength < self.size() ? std::wstring(self.data(), selfLength) : std::wstring(); + exeDirectory.resize(exeDirectory.find_last_of(L"\\/") + 1); + std::wstring input = argc == 2 ? FullPath(argv[1]) : exeDirectory; + if (input.empty() || exeDirectory.empty()) + { + wprintf(L"resolving paths failed: %lu\n", GetLastError()); + return 1; + } + + const Target* target = nullptr; + DWORD processId = 0; + std::wstring assembly; + if (IsFile(input)) + { + PeImage image; + WORD machine = image.Load(input) ? image.Machine() : 0; + target = machine == IMAGE_FILE_MACHINE_I386 ? &Targets[0] : machine == IMAGE_FILE_MACHINE_AMD64 ? &Targets[1] : nullptr; + if (!target) + { + wprintf(L"not an x86 or x64 TerraSharp build: %s\n", input.c_str()); + return 1; + } + assembly = input; + processId = FindProcess(target->game); + } + else + { + DWORD terraria = FindProcess(Game::Terraria), tModLoader = FindProcess(Game::TModLoader); + if (terraria && tModLoader) + { + wprintf(L"Terraria and tModLoader are both running; pass the DLL to inject\n"); + return 2; + } + target = tModLoader ? &Targets[1] : &Targets[0]; + processId = tModLoader ? tModLoader : terraria; + std::wstring directory = WithSlash(input); + assembly = FindAssembly(directory, *target); + if (processId && assembly.empty()) + { + wprintf(L"expected exactly one %s*.dll in %s\n", target->assembly, directory.c_str()); + return 1; + } + } + if (!IsFile(assembly)) + { + wprintf(L"managed DLL not found: %s\n", assembly.c_str()); + return 1; + } + if (!processId) + { + wprintf(L"%s is not running\n", target->name); + return 2; + } + Handle process(OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, processId)); + if (!process.value) + { + wprintf(L"OpenProcess failed: %lu\n", GetLastError()); + return 3; + } + return Inject(process.value, processId, *target, assembly); +} diff --git a/src/TerraSharp/Autoload/Autoload.cs b/src/TerraSharp/Autoload/Autoload.cs new file mode 100644 index 0000000..eed4e6c --- /dev/null +++ b/src/TerraSharp/Autoload/Autoload.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using Terraria; +using TerraSharp.Core; + +namespace TerraSharp; + +internal static class Autoload +{ + static int _started; + + public static void Start() + { + if (Interlocked.Exchange(ref _started, 1) == 0) + new Thread(Run) { IsBackground = true, Name = "TerraSharp loader" }.Start(); + } + + static void Run() + { + try + { + WaitForEngine(); + Load(); + } + catch (Exception e) + { + Log.Error("Autoload failed", e); + } + } + + static void WaitForEngine() + { + // Wait for the window before accessing Main to avoid running its static constructor on this thread. + using Process process = Process.GetCurrentProcess(); + while (process.MainWindowHandle == IntPtr.Zero) + { + Thread.Sleep(100); + process.Refresh(); + } + + // Set right before Main.OnEngineLoad on both Terraria and tModLoader. + FieldInfo engineLoaded = typeof(Main).GetField("IsEngineLoaded", BindingFlags.Static | BindingFlags.NonPublic)!; + while (!(bool)engineLoaded.GetValue(null)!) + Thread.Sleep(100); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void Load() + { + Log.Info("Autoload " + typeof(Autoload).Assembly.FullName + " from " + typeof(Autoload).Assembly.Location); + Plugin.Load(); + } +} diff --git a/src/TerraSharp/Autoload/DomainManager.cs b/src/TerraSharp/Autoload/DomainManager.cs new file mode 100644 index 0000000..fe4d869 --- /dev/null +++ b/src/TerraSharp/Autoload/DomainManager.cs @@ -0,0 +1,22 @@ +using System; +using TerraSharp.Core; + +namespace TerraSharp; + +// Registered through appDomainManagerType in Terraria.exe.config. +public sealed class LoaderDomainManager : AppDomainManager +{ + public override void InitializeNewDomain(AppDomainSetup setup) + { + try + { + AppDomain.CurrentDomain.AssemblyResolve += Entry.ResolveEmbedded; + Autoload.Start(); + } + catch (Exception e) + { + Log.Error("Autoload init failed", e); + } + base.InitializeNewDomain(setup); + } +} diff --git a/src/TerraSharp/Core/Clock.cs b/src/TerraSharp/Core/Clock.cs new file mode 100644 index 0000000..63d6aa5 --- /dev/null +++ b/src/TerraSharp/Core/Clock.cs @@ -0,0 +1,9 @@ +namespace TerraSharp.Core; + +// Advanced by the local Player.Update postfix. +public static class Clock +{ + public static int Tick { get; private set; } + + public static void Advance() => Tick++; +} diff --git a/src/TerraSharp/Core/GameMouse.cs b/src/TerraSharp/Core/GameMouse.cs new file mode 100644 index 0000000..4ec7b93 --- /dev/null +++ b/src/TerraSharp/Core/GameMouse.cs @@ -0,0 +1,19 @@ +using Microsoft.Xna.Framework; +using Terraria; + +namespace TerraSharp.Core; + +// Terraria aims at Main.mouseX/Y + screenPosition. +public static class GameMouse +{ + public static (int X, int Y) Save() => (Main.mouseX, Main.mouseY); + + public static void Restore((int X, int Y) saved) => (Main.mouseX, Main.mouseY) = saved; + + public static void PointAt(Vector2 world) + { + Main.mouseX = (int)(world.X - Main.screenPosition.X); + float y = world.Y - Main.screenPosition.Y; + Main.mouseY = (int)(Main.LocalPlayer.gravDir == -1f ? Main.screenHeight - y : y); + } +} diff --git a/src/TerraSharp/Core/Hotkeys.cs b/src/TerraSharp/Core/Hotkeys.cs new file mode 100644 index 0000000..693b1fe --- /dev/null +++ b/src/TerraSharp/Core/Hotkeys.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using Terraria; +using TerraSharp.Features; +using TerraSharp.Features.Combat; +using TerraSharp.UI; + +namespace TerraSharp.Core; + +public static class Hotkeys +{ + public sealed record Bind(string Group, string Id, string Label, Action Run); + + const string InterfaceGroup = "Interface"; + + static Bind Toggle(string group, string id, string label, Func get, Action set) => new(group, id, label, () => + { + bool enabled = !get(); + set(enabled); + Notify(label + (enabled ? ": On" : ": Off"), enabled ? Widgets.On : new Color(200, 200, 200)); + }); + + public static readonly IReadOnlyList All = + [ + new(InterfaceGroup, "Menu", "Open / close menu", () => WindowManager.Toggle(WindowManager.Menu)), + new(InterfaceGroup, "ItemBrowser", "Item Browser", () => WindowManager.Toggle(WindowManager.Items)), + new(InterfaceGroup, "BuffBrowser", "Buff Browser", () => WindowManager.Toggle(WindowManager.Buffs)), + + Toggle("Player", "GodMode", "God Mode", () => Settings.GodMode, v => Settings.GodMode = v), + Toggle("Player", "AutoHeal", "Auto Heal", () => Settings.AutoHeal, v => Settings.AutoHeal = v), + Toggle("Player", "AutoMana", "Auto Mana", () => Settings.AutoMana, v => Settings.AutoMana = v), + new("Player", "FullHeal", "Full Heal", PlayerFeatures.FullHeal), + new("Player", "TpSpawn", "Teleport to Spawn", PlayerFeatures.TeleportSpawn), + new("Player", "TpCursor", "Teleport to Cursor", PlayerFeatures.TeleportCursor), + + Toggle("Movement", "Fly", "Fly", () => Settings.Fly, v => Settings.Fly = v), + Toggle("Movement", "SuperSpeed", "Super Speed", () => Settings.SuperSpeed, v => Settings.SuperSpeed = v), + Toggle("Movement", "GravityFlip", "Gravity Flip", () => Settings.GravityFlip, v => Settings.GravityFlip = v), + Toggle("Movement", "ExtendedReach", "Extended Reach", () => Settings.ExtendedReach, v => Settings.ExtendedReach = v), + Toggle("Movement", "HighJump", "High Jump", () => Settings.HighJump, v => Settings.HighJump = v), + Toggle("Movement", "WaterWalk", "Water Walk", () => Settings.WaterWalk, v => Settings.WaterWalk = v), + Toggle("Movement", "InfFlight", "Infinite Flight", () => Settings.InfFlight, v => Settings.InfFlight = v), + + Toggle("Combat", "Aimbot", "Aimbot", () => Settings.Aimbot, v => Settings.Aimbot = v), + Toggle("Combat", "NoSpread", "No Spread", () => Settings.NoSpread, v => Settings.NoSpread = v), + Toggle("Combat", "AutoAttack", "Auto Attack", () => Settings.AutoAttack, v => Settings.AutoAttack = v), + Toggle("Combat", "KillAura", "Kill Aura", () => Settings.KillAura, v => Settings.KillAura = v), + Toggle("Combat", "HitMagnet", "Hit Magnet", () => Settings.HitMagnet, v => Settings.HitMagnet = v), + Toggle("Combat", "FreezeEnemies", "Freeze Enemies", () => Settings.FreezeEnemies, v => Settings.FreezeEnemies = v), + Toggle("Combat", "WeakenEnemies", "Weaken Enemies", () => Settings.WeakenEnemies, v => Settings.WeakenEnemies = v), + Toggle("Combat", "RapidAttack", "Rapid Attack", () => Settings.RapidAttack, v => Settings.RapidAttack = v), + new("Combat", "GatherAll", "Gather All Enemies", CombatFeatures.GatherAll), + new("Combat", "KillAll", "Kill All Enemies", CombatFeatures.KillAll), + + Toggle("Items", "AutoFish", "Auto Fishing", () => Settings.AutoFish, v => Settings.AutoFish = v), + Toggle("Items", "ItemVacuum", "Item Vacuum", () => Settings.ItemVacuum, v => Settings.ItemVacuum = v), + Toggle("Items", "InstantBreak", "Instant Break", () => Settings.InstantBreak, v => Settings.InstantBreak = v), + Toggle("Items", "InfItems", "Infinite Items", () => Settings.InfItems, v => Settings.InfItems = v), + + Toggle("Visuals", "Esp", "ESP", () => Settings.Esp, v => Settings.Esp = v), + Toggle("Visuals", "Spelunker", "Spelunker Vision", () => Settings.Spelunker, v => Settings.Spelunker = v), + Toggle("Visuals", "Hunter", "Hunter", () => Settings.Hunter, v => Settings.Hunter = v), + Toggle("Visuals", "DangerSense", "Danger Sense", () => Settings.DangerSense, v => Settings.DangerSense = v), + Toggle("Visuals", "BiomeSight", "Biome Sight", () => Settings.BiomeSight, v => Settings.BiomeSight = v), + Toggle("Visuals", "NightOwl", "Night Owl", () => Settings.NightOwl, v => Settings.NightOwl = v), + Toggle("Visuals", "Shine", "Shine", () => Settings.Shine, v => Settings.Shine = v), + + Toggle("World", "FreezeTime", "Freeze Time", () => Settings.FreezeTime, v => Settings.FreezeTime = v), + new("World", "SavePosition", "Save Current Position", Waypoints.AddHere), + ]; + + // Windows virtual-key codes for mouse buttons; XNA's Keys leaves them unassigned. + const Keys MiddleMouse = (Keys)4; + const Keys Mouse4 = (Keys)5; + const Keys Mouse5 = (Keys)6; + static readonly Keys[] MouseKeys = [MiddleMouse, Mouse4, Mouse5]; + + public static Bind? Capturing; + static KeyboardState _previousKeyboard; + static MouseState _previousMouse; + + public static void Notify(string text, Color color) + { + if (!Main.gameMenu) + CombatText.NewText(Main.LocalPlayer.Hitbox, color, text, false, false); + } + + public static string KeyName(Keys key) => key switch + { + Keys.None => "None", + MiddleMouse => "Middle Mouse", + Mouse4 => "Mouse 4", + Mouse5 => "Mouse 5", + _ => key.ToString().Replace("OemTilde", "~").Replace("Oem", "").Replace("NumPad", "Num "), + }; + + public static Keys Get(string id) => Settings.Binds.TryGetValue(id, out Keys key) ? key : Keys.None; + + public static void Clear(string id) => Settings.Binds[id] = Keys.None; + + static bool IsDown(Keys key, KeyboardState keyboard, MouseState mouse) => key switch + { + MiddleMouse => mouse.MiddleButton == ButtonState.Pressed, + Mouse4 => mouse.XButton1 == ButtonState.Pressed, + Mouse5 => mouse.XButton2 == ButtonState.Pressed, + _ => keyboard.IsKeyDown(key), + }; + + static bool Pressed(Keys key, KeyboardState keyboard, MouseState mouse) => IsDown(key, keyboard, mouse) && !IsDown(key, _previousKeyboard, _previousMouse); + + public static void Update() + { + KeyboardState keyboard = Keyboard.GetState(); + MouseState mouse = Mouse.GetState(); + if (Capturing is not null) + { + Capture(keyboard, mouse); + _previousKeyboard = keyboard; + _previousMouse = mouse; + return; + } + + bool blocked = !Main.instance.IsActive || WindowManager.Focus is not null || Main.drawingPlayerChat || Main.editSign || Main.editChest; + if (!blocked) + foreach (Bind bind in All) + { + if (Main.gameMenu && bind.Group != InterfaceGroup) + continue; + + Keys key = Get(bind.Id); + if (key != Keys.None && Pressed(key, keyboard, mouse)) + bind.Run(); + } + _previousKeyboard = keyboard; + _previousMouse = mouse; + } + + static void Capture(KeyboardState keyboard, MouseState mouse) + { + foreach (Keys key in keyboard.GetPressedKeys().Concat(MouseKeys)) + { + if (!Pressed(key, keyboard, mouse)) + continue; + + switch (key) + { + case Keys.Escape: + break; + case Keys.Back or Keys.Delete: + Clear(Capturing!.Id); + Widgets.Sound(12); + break; + default: + Settings.Binds[Capturing!.Id] = key; + Widgets.Sound(12); + break; + } + + WindowManager.Unfocus(); + return; + } + } +} diff --git a/src/TerraSharp/Core/Json.cs b/src/TerraSharp/Core/Json.cs new file mode 100644 index 0000000..6384bd0 --- /dev/null +++ b/src/TerraSharp/Core/Json.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Text; + +namespace TerraSharp.Core; + +// Objects parse to Dictionary, arrays to List, numbers to double. +public static class Json +{ + public static string Serialize(object? value) + { + var builder = new StringBuilder(); + Write(builder, value, 0); + return builder.ToString(); + } + + public static object? Parse(string text) + { + var reader = new Reader(text); + object? value = reader.Value(); + reader.End(); + return value; + } + + public static T ConvertTo(object? value) => (T)ConvertTo(typeof(T), value)!; + + static object? ConvertTo(Type type, object? value) + { + if (value is null) + return type.IsValueType ? Activator.CreateInstance(type) : null; + if (type.IsInstanceOfType(value)) + return value; + if (type.IsEnum) + return value is string stringValue ? Enum.Parse(type, stringValue) : Enum.ToObject(type, Convert.ToInt64(value, CultureInfo.InvariantCulture)); + if (type.IsPrimitive || type == typeof(string) || type == typeof(decimal)) + return Convert.ChangeType(value, type, CultureInfo.InvariantCulture); + + if (type.IsArray && value is List items) + { + Type element = type.GetElementType()!; + var array = Array.CreateInstance(element, items.Count); + for (int i = 0; i < items.Count; i++) + array.SetValue(ConvertTo(element, items[i]), i); + return array; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>) && value is List list) + { + var result = (IList)Activator.CreateInstance(type)!; + Type element = type.GetGenericArguments()[0]; + foreach (object item in list) + result.Add(ConvertTo(element, item)); + return result; + } + + if (value is Dictionary fields) + { + object result = Activator.CreateInstance(type)!; + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + if (property.CanWrite && fields.TryGetValue(property.Name, out object? field)) + property.SetValue(result, ConvertTo(property.PropertyType, field)); + return result; + } + + throw new FormatException($"cannot convert {value.GetType().Name} to {type.Name}"); + } + + static void Write(StringBuilder builder, object? value, int depth) + { + switch (value) + { + case null: + builder.Append("null"); + break; + case bool booleanValue: + builder.Append(booleanValue ? "true" : "false"); + break; + case string stringValue: + WriteString(builder, stringValue); + break; + case float floatValue: + builder.Append(floatValue.ToString("R", CultureInfo.InvariantCulture)); + break; + case double doubleValue: + builder.Append(doubleValue.ToString("R", CultureInfo.InvariantCulture)); + break; + case Enum enumValue: + WriteString(builder, enumValue.ToString()); + break; + case IConvertible convertibleValue when convertibleValue.GetTypeCode() is not (TypeCode.Object or TypeCode.Char or TypeCode.DateTime): + builder.Append(convertibleValue.ToString(CultureInfo.InvariantCulture)); + break; + case IDictionary dictionary: + { + var entries = new List>(); + foreach (DictionaryEntry entry in dictionary) + entries.Add(new(Convert.ToString(entry.Key, CultureInfo.InvariantCulture)!, entry.Value)); + + WriteObject(builder, entries, depth); + break; + } + case IEnumerable items: + { + List list = [.. items]; + if (list.Count == 0) + { + builder.Append("[]"); + break; + } + + builder.Append('['); + for (int i = 0; i < list.Count; i++) + { + builder.Append(i == 0 ? "" : ","); + NewLine(builder, depth + 1); + Write(builder, list[i], depth + 1); + } + + NewLine(builder, depth); + builder.Append(']'); + break; + } + default: + { + var entries = new List>(); + foreach (PropertyInfo property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + if (property.CanRead && property.GetIndexParameters().Length == 0) + entries.Add(new(property.Name, property.GetValue(value))); + + WriteObject(builder, entries, depth); + break; + } + } + } + + static void WriteObject(StringBuilder builder, List> entries, int depth) + { + if (entries.Count == 0) + { + builder.Append("{}"); + return; + } + + builder.Append('{'); + for (int i = 0; i < entries.Count; i++) + { + builder.Append(i == 0 ? "" : ","); + NewLine(builder, depth + 1); + WriteString(builder, entries[i].Key); + builder.Append(": "); + Write(builder, entries[i].Value, depth + 1); + } + + NewLine(builder, depth); + builder.Append('}'); + } + + static void NewLine(StringBuilder builder, int depth) => builder.Append('\n').Append(' ', depth * 2); + + static void WriteString(StringBuilder builder, string text) + { + builder.Append('"'); + foreach (char character in text) + { + switch (character) + { + case '"': builder.Append("\\\""); break; + case '\\': builder.Append("\\\\"); break; + case '\n': builder.Append("\\n"); break; + case '\r': builder.Append("\\r"); break; + case '\t': builder.Append("\\t"); break; + case < ' ': builder.Append("\\u").Append(((int)character).ToString("x4", CultureInfo.InvariantCulture)); break; + default: builder.Append(character); break; + } + } + builder.Append('"'); + } + + sealed class Reader(string text) + { + int _position; + + public void End() + { + Skip(); + if (_position < text.Length) + throw Error("trailing characters"); + } + + public object? Value() + { + Skip(); + + if (_position >= text.Length) + throw Error("unexpected end"); + + switch (text[_position]) + { + case '{': + return Object(); + case '[': + return Array(); + case '"': + return String(); + case 't': + Literal("true"); + return true; + case 'f': + Literal("false"); + return false; + case 'n': + Literal("null"); + return null; + default: + return Number(); + } + } + + Dictionary Object() + { + var result = new Dictionary(); + _position++; + if (Peek() == '}') + { + _position++; + return result; + } + + while (true) + { + Skip(); + if (Peek() != '"') + throw Error("expected property name"); + + string key = String(); + Expect(':'); + result[key] = Value()!; + + char next = Peek(); + _position++; + if (next == '}') + return result; + if (next != ',') + throw Error("expected ',' or '}'"); + } + } + + List Array() + { + var result = new List(); + _position++; + if (Peek() == ']') + { + _position++; + return result; + } + + while (true) + { + result.Add(Value()!); + + char next = Peek(); + _position++; + if (next == ']') + return result; + if (next != ',') + throw Error("expected ',' or ']'"); + } + } + + string String() + { + var builder = new StringBuilder(); + _position++; + + while (true) + { + if (_position >= text.Length) + throw Error("unterminated string"); + + char character = text[_position++]; + if (character == '"') + return builder.ToString(); + if (character != '\\') + { + builder.Append(character); + continue; + } + + if (_position >= text.Length) + throw Error("unterminated string"); + + char escape = text[_position++]; + builder.Append(escape switch + { + 'b' => '\b', + 'f' => '\f', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'u' => Unicode(), + '"' or '\\' or '/' => escape, + _ => throw Error("bad escape"), + }); + } + } + + char Unicode() + { + if (_position + 4 > text.Length || !int.TryParse(text.Substring(_position, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int code)) + throw Error("bad escape"); + + _position += 4; + return (char)code; + } + + double Number() + { + int start = _position; + while (_position < text.Length && text[_position] is '-' or '+' or '.' or 'e' or 'E' or (>= '0' and <= '9')) + _position++; + if (_position == start || !double.TryParse(text.Substring(start, _position - start), NumberStyles.Float, CultureInfo.InvariantCulture, out double value)) + throw Error("bad value"); + return value; + } + + void Literal(string literal) + { + if (string.CompareOrdinal(text, _position, literal, 0, literal.Length) != 0) + throw Error("bad value"); + + _position += literal.Length; + } + + void Expect(char character) + { + if (Peek() != character) + throw Error($"expected '{character}'"); + _position++; + } + + char Peek() + { + Skip(); + return _position < text.Length ? text[_position] : '\0'; + } + + void Skip() + { + while (_position < text.Length && text[_position] is ' ' or '\t' or '\r' or '\n' or '') + _position++; + } + + FormatException Error(string message) => new($"json: {message} at {_position}"); + } +} diff --git a/src/TerraSharp/Core/Log.cs b/src/TerraSharp/Core/Log.cs new file mode 100644 index 0000000..6fd07d6 --- /dev/null +++ b/src/TerraSharp/Core/Log.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace TerraSharp.Core; + +public static class Log +{ + enum Level { Debug, Info, Warning, Error } + +#if TERRASHARP_LOGGING + static readonly string LogPath = Path.Combine(Path.GetTempPath(), "terrasharp.log"); + static readonly object SyncLock = new(); + static readonly Dictionary Seen = []; +#endif + + [Conditional("TERRASHARP_LOGGING")] + public static void Debug(string message) => Write(message, Level.Debug); + + [Conditional("TERRASHARP_LOGGING")] + public static void Info(string message) => Write(message, Level.Info); + + [Conditional("TERRASHARP_LOGGING")] + public static void Warning(string message) => Write(message, Level.Warning); + + [Conditional("TERRASHARP_LOGGING")] + public static void Error(string message) => Write(message, Level.Error); + + // Per-frame code: each distinct failure is logged at most three times. + [Conditional("TERRASHARP_LOGGING")] + public static void Error(string where, Exception exception) + { +#if TERRASHARP_LOGGING + lock (SyncLock) + { + string key = where + exception.GetType().Name + exception.Message; + Seen.TryGetValue(key, out int count); + if (count >= 3) + return; + + Seen[key] = count + 1; + Write($"{where}: {exception}", Level.Error); + } +#endif + } + + [Conditional("TERRASHARP_LOGGING")] + static void Write(string message, Level level) + { +#if TERRASHARP_LOGGING + try + { + lock (SyncLock) + File.AppendAllText(LogPath, $"{DateTime.Now:HH:mm:ss.fff} [{level.ToString().ToUpperInvariant()}] {message}{Environment.NewLine}"); + } + catch + { + } +#endif + } +} diff --git a/src/TerraSharp/Core/Plugin.cs b/src/TerraSharp/Core/Plugin.cs new file mode 100644 index 0000000..0594ffd --- /dev/null +++ b/src/TerraSharp/Core/Plugin.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading; +using TerraSharp.Features; +using TerraSharp.Patches; +using TerraSharp.UI; + +namespace TerraSharp.Core; + +public static class Plugin +{ + public const string Id = "terrasharp"; + const string ReloadKey = "TerraSharp.Unload"; + static readonly object ReloadLock = GetReloadLock(); + + static readonly object SyncLock = new(); + static HookRegistry? _hooks; + static readonly ManualResetEventSlim Unloaded = new(true); + static volatile bool _installPending, _unloadPending; + static volatile bool _active; + + public static bool Active => _active; + + static object GetReloadLock() + { + lock (AppDomain.CurrentDomain) + { + const string key = "TerraSharp.ReloadLock"; + object value = AppDomain.CurrentDomain.GetData(key) ?? new object(); + AppDomain.CurrentDomain.SetData(key, value); + return value; + } + } + + // The injector installs hooks; Tick initializes game state on the main thread. + public static void Load() + { + lock (ReloadLock) + { + if (Active) + return; + + (AppDomain.CurrentDomain.GetData(ReloadKey) as Action)?.Invoke(); + + lock (SyncLock) + { + _hooks = HookRegistry.Install(); + Unloaded.Reset(); + _active = true; + _installPending = true; + AppDomain.CurrentDomain.SetData(ReloadKey, (Action)UnloadForReload); + Log.Info("Loaded"); + } + } + } + + public static void RequestUnload() => _unloadPending = true; + + static void UnloadForReload() + { + if (Active) + RequestUnload(); + + if (!Unloaded.Wait(TimeSpan.FromSeconds(10))) + throw new TimeoutException("The previous TerraSharp build did not finish unloading."); + } + + // Main.DoUpdate postfix: main thread, outside drawing. + public static void Tick() + { + if (_installPending) + { + _installPending = false; + Settings.Load(); + InterfaceLayers.Install(); + WindowManager.Show(WindowManager.Menu); + } + + if (_unloadPending) + { + _unloadPending = false; + Unload(); + } + } + + // Injected assemblies remain loaded after their hooks are removed. + static void Unload() + { + if (!Active) + return; + + _active = false; + Guard.Run("Unload settings", Settings.Flush); + Guard.Run("Unload windows", WindowManager.HideAll); + Guard.Run("Unload mouse", InterfaceLayers.Unblock); + Guard.Run("Unload layers", InterfaceLayers.Uninstall); + Guard.Run("Unload item overrides", HeldItemOverrides.Restore); + Guard.Run("Unload player overrides", PlayerFeatures.OnUnload); + + HookRegistry hooks = _hooks!; + // Hook disposal waits for active calls, including the current update hook. + new Thread(() => + { + lock (SyncLock) + { + try + { + hooks.Dispose(); + _hooks = null; + Unloaded.Set(); + Log.Info("Unloaded"); + } + catch (Exception e) + { + Log.Error("Hook removal failed", e); + } + } + }) { IsBackground = true }.Start(); + } +} diff --git a/src/TerraSharp/Core/Profiler.cs b/src/TerraSharp/Core/Profiler.cs new file mode 100644 index 0000000..5816f48 --- /dev/null +++ b/src/TerraSharp/Core/Profiler.cs @@ -0,0 +1,120 @@ +using System; +using System.Diagnostics; +#if TERRASHARP_LOGGING +using System.Text; +#endif + +namespace TerraSharp.Core; + +public enum Feature { None, Aimbot, AutoAttack, NpcMotion, ShotPreview, Esp } + +public enum Impact { Idle, Negligible, Low, Medium, High } + +// Exclusive main-thread time per feature: a nested scope pauses the one around it. +public static class Profiler +{ + public const float FrameBudget = 1000f / 60f; + const int WindowTicks = 30; + const int MaxDepth = 16; + + static readonly int Count = Enum.GetValues(typeof(Feature)).Length; + static readonly double MsPerTimestamp = 1000.0 / Stopwatch.Frequency; + static readonly long[] Current = new long[Count], Window = new long[Count], WindowPeak = new long[Count]; + static readonly float[] Averages = new float[Count], Peaks = new float[Count]; + static readonly bool[] Ran = new bool[Count]; + static readonly Feature[] Stack = new Feature[MaxDepth]; + static int _depth, _overflow, _windowTicks; + static Feature _running; + static long _since; +#if TERRASHARP_LOGGING + static int _windows; +#endif + + public readonly struct Scope : IDisposable + { + public void Dispose() => End(); + } + + public static Scope Measure(Feature feature) + { + if (_depth == MaxDepth) + { + _overflow++; + return default; + } + + long now = Stopwatch.GetTimestamp(); + Current[(int)_running] += now - _since; + Stack[_depth++] = _running; + (_running, _since) = (feature, now); + return default; + } + + static void End() + { + if (_overflow > 0) + { + _overflow--; + return; + } + + long now = Stopwatch.GetTimestamp(); + Current[(int)_running] += now - _since; + _running = _depth > 0 ? Stack[--_depth] : Feature.None; + _since = now; + } + + // Main.DoUpdate postfix; draws since the previous update count toward this tick. + public static void EndTick() + { + for (int i = 1; i < Count; i++) + { + Window[i] += Current[i]; + WindowPeak[i] = Math.Max(WindowPeak[i], Current[i]); + Current[i] = 0; + } + + if (++_windowTicks < WindowTicks) + return; + + for (int i = 1; i < Count; i++) + { + float average = (float)(Window[i] * MsPerTimestamp / WindowTicks); + Averages[i] += (average - Averages[i]) * 0.5f; + Peaks[i] = (float)(WindowPeak[i] * MsPerTimestamp); + Ran[i] = Window[i] > 0; + Window[i] = WindowPeak[i] = 0; + } + _windowTicks = 0; + + LogSummary(); + } + + public static float Average(Feature feature) => Averages[(int)feature]; + + public static float Peak(Feature feature) => Peaks[(int)feature]; + + // A single tick at four times the average budget share stutters as much as a steady cost. + public static Impact ImpactOf(Feature feature) => !Ran[(int)feature] ? Impact.Idle : Math.Max(Average(feature), Peak(feature) / 4f) switch + { + < 0.05f => Impact.Negligible, + < 0.5f => Impact.Low, + < 2f => Impact.Medium, + _ => Impact.High, + }; + + [Conditional("TERRASHARP_LOGGING")] + static void LogSummary() + { +#if TERRASHARP_LOGGING + if (++_windows % 20 != 0) + return; + + var text = new StringBuilder("Profiler (ms per tick, avg/peak):"); + for (int i = 1; i < Count; i++) + if (Ran[i]) + text.Append($" {(Feature)i} {Averages[i]:0.000}/{Peaks[i]:0.00} {ImpactOf((Feature)i)};"); + Log.Debug(text.ToString()); +#endif + } +} diff --git a/src/TerraSharp/Core/Settings.Json.cs b/src/TerraSharp/Core/Settings.Json.cs new file mode 100644 index 0000000..abac43a --- /dev/null +++ b/src/TerraSharp/Core/Settings.Json.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; + +namespace TerraSharp.Core; + +public static partial class Settings +{ + static readonly Dictionary Paths = new() + { + [nameof(GodMode)] = "Player.GodMode", + [nameof(InfHealth)] = "Player.InfiniteHealth", + [nameof(InfMana)] = "Player.InfiniteMana", + [nameof(InfBreath)] = "Player.InfiniteBreath", + [nameof(InfFlight)] = "Player.InfiniteFlight", + [nameof(InstantRespawn)] = "Player.InstantRespawn", + [nameof(AutoHeal)] = "Player.AutoHeal.Enabled", + [nameof(AutoHealAt)] = "Player.AutoHeal.Threshold", + [nameof(AutoMana)] = "Player.AutoMana.Enabled", + [nameof(AutoManaAt)] = "Player.AutoMana.Threshold", + [nameof(BuffMinutes)] = "Player.Buffs.Minutes", + + [nameof(Fly)] = "Movement.Fly.Enabled", + [nameof(FlySpeed)] = "Movement.Fly.Speed", + [nameof(SuperSpeed)] = "Movement.SuperSpeed.Enabled", + [nameof(SpeedMult)] = "Movement.SuperSpeed.Multiplier", + [nameof(HighJump)] = "Movement.HighJump", + [nameof(NoFallDamage)] = "Movement.NoFallDamage", + [nameof(LavaImmune)] = "Movement.LavaImmune", + [nameof(WaterWalk)] = "Movement.WaterWalk", + [nameof(NoKnockback)] = "Movement.NoKnockback", + [nameof(GravityFlip)] = "Movement.GravityFlip", + [nameof(ExtendedReach)] = "Movement.ExtendedReach.Enabled", + [nameof(ReachX)] = "Movement.ExtendedReach.Horizontal", + [nameof(ReachY)] = "Movement.ExtendedReach.Vertical", + + [nameof(RapidAttack)] = "Combat.Weapon.RapidAttack", + [nameof(AutoSwing)] = "Combat.Weapon.AutoSwing", + [nameof(DamageBoost)] = "Combat.Weapon.DamageBoost.Enabled", + [nameof(DamageMult)] = "Combat.Weapon.DamageBoost.Multiplier", + [nameof(NoManaCost)] = "Combat.Weapon.NoManaCost", + [nameof(NoSpread)] = "Combat.Weapon.NoSpread", + + [nameof(KillAura)] = "Combat.KillAura.Enabled", + [nameof(KillAuraRange)] = "Combat.KillAura.Range", + [nameof(KillAuraInterval)] = "Combat.KillAura.Interval", + [nameof(HitMagnet)] = "Combat.HitMagnet.Enabled", + [nameof(HitMagnetRange)] = "Combat.HitMagnet.Range", + [nameof(FreezeEnemies)] = "Combat.FreezeEnemies", + [nameof(WeakenEnemies)] = "Combat.WeakenEnemies", + + [nameof(Aimbot)] = "Combat.Aim.Aimbot", + [nameof(AimPrediction)] = "Combat.Aim.Prediction", + [nameof(AimDrawTarget)] = "Combat.Aim.ShowTarget", + [nameof(AutoAttack)] = "Combat.Aim.AutoAttack", + [nameof(AimExact)] = "Combat.Aim.ExactCheck", + [nameof(ShotPreview)] = "Combat.Aim.ShotPreview", + [nameof(AimFov)] = "Combat.Aim.CursorRadius", + [nameof(AimRange)] = "Combat.Aim.Range", + + [nameof(InstantBreak)] = "Items.InstantBreak", + [nameof(InfItems)] = "Items.InfiniteItems", + [nameof(ItemVacuum)] = "Items.Vacuum", + + [nameof(AutoFish)] = "Fishing.Enabled", + [nameof(AutoRecast)] = "Fishing.Recast", + [nameof(FishEnemies)] = "Fishing.CatchEnemies", + [nameof(Sonar)] = "Fishing.Sonar", + + [nameof(Esp)] = "Esp.Enabled", + [nameof(EspNames)] = "Esp.Names", + [nameof(EspHealth)] = "Esp.HealthBars", + [nameof(EspHealthText)] = "Esp.HealthNumbers", + [nameof(EspDistance)] = "Esp.Distance", + [nameof(EspLines)] = "Esp.Tracers", + [nameof(EspProjPath)] = "Esp.ProjectilePaths", + [nameof(EspStyle)] = "Esp.Style", + [nameof(EspThickness)] = "Esp.Thickness", + [nameof(EspMaxDistance)] = "Esp.MaxDistance", + [nameof(EspLocal)] = "Esp.LocalPlayer.Enabled", + [nameof(ColLocal)] = "Esp.LocalPlayer.Color", + [nameof(EspPlayers)] = "Esp.Players.Enabled", + [nameof(ColPlayers)] = "Esp.Players.Color", + [nameof(EspHostile)] = "Esp.HostileNpcs.Enabled", + [nameof(ColHostile)] = "Esp.HostileNpcs.Color", + [nameof(EspCritters)] = "Esp.Critters.Enabled", + [nameof(ColCritters)] = "Esp.Critters.Color", + [nameof(EspTown)] = "Esp.TownNpcs.Enabled", + [nameof(ColTown)] = "Esp.TownNpcs.Color", + [nameof(EspBossRainbow)] = "Esp.Bosses.Rainbow", + [nameof(ColBoss)] = "Esp.Bosses.Color", + [nameof(EspItems)] = "Esp.Items.Enabled", + [nameof(ColItems)] = "Esp.Items.Color", + [nameof(EspProjHostile)] = "Esp.HostileProjectiles.Enabled", + [nameof(ColProjHostile)] = "Esp.HostileProjectiles.Color", + [nameof(EspProjFriendly)] = "Esp.FriendlyProjectiles.Enabled", + [nameof(ColProjFriendly)] = "Esp.FriendlyProjectiles.Color", + [nameof(EspWaypoints)] = "Esp.Waypoints.Enabled", + [nameof(ColWaypoint)] = "Esp.Waypoints.Color", + + [nameof(Spelunker)] = "Vision.Spelunker", + [nameof(Hunter)] = "Vision.Hunter", + [nameof(Shine)] = "Vision.Shine", + [nameof(NightOwl)] = "Vision.NightOwl", + [nameof(DangerSense)] = "Vision.DangerSense", + [nameof(BiomeSight)] = "Vision.BiomeSight", + + [nameof(FreezeTime)] = "World.FreezeTime", + + [nameof(LogBallistics)] = "Ballistics.Log", + }; + + static void SetValue(Dictionary document, string path, object value) + { + string[] parts = path.Split('.'); + for (int i = 0; i < parts.Length - 1; i++) + { + if (!document.TryGetValue(parts[i], out object? section)) + document[parts[i]] = section = new Dictionary(); + document = (Dictionary)section; + } + + document[parts[parts.Length - 1]] = value; + } + + static bool TryGetValue(Dictionary document, string path, out object? value) + { + value = document; + foreach (string part in path.Split('.')) + if (value is not Dictionary section || !section.TryGetValue(part, out value)) + return false; + return true; + } + + static Dictionary Section(Dictionary document, string path) => TryGetValue(document, path, out object? value) && value is Dictionary section ? section : []; +} diff --git a/src/TerraSharp/Core/Settings.cs b/src/TerraSharp/Core/Settings.cs new file mode 100644 index 0000000..4678366 --- /dev/null +++ b/src/TerraSharp/Core/Settings.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using Terraria; +using TerraSharp.Features; +using TerraSharp.Simulation; +using TerraSharp.UI; + +namespace TerraSharp.Core; + +public enum EspBoxStyle +{ + Outline, + Corners, + Filled, +} + +// Public mutable fields are persisted through Settings.Json.cs. +public static partial class Settings +{ + public static bool GodMode, InfHealth, InfMana, InfBreath, InfFlight, InstantRespawn; + public static bool AutoHeal, AutoMana; + public static float AutoHealAt = 40f, AutoManaAt = 25f, BuffMinutes = 10f; + + public static bool Fly, SuperSpeed, HighJump, NoFallDamage, LavaImmune, WaterWalk, NoKnockback, GravityFlip, ExtendedReach; + public static float FlySpeed = 12f, SpeedMult = 3f; + public static int ReachX = 35, ReachY = 30; + + public static bool RapidAttack, AutoSwing, DamageBoost, NoManaCost, NoSpread; + public static float DamageMult = 5f; + public static bool KillAura, HitMagnet, FreezeEnemies, WeakenEnemies; + public static float KillAuraRange = 20f, HitMagnetRange = 30f; + public static int KillAuraInterval = 10; + public static bool Aimbot, AimPrediction = true, AimDrawTarget = true, AutoAttack, AimExact = true, ShotPreview; + public static float AimFov = 250f, AimRange = 60f; + + public static bool InstantBreak, InfItems, ItemVacuum; + public static bool AutoFish, AutoRecast = true, FishEnemies, Sonar; + + public static bool Esp, EspNames = true, EspHealth = true, EspHealthText, EspDistance, EspLines, EspProjPath = true; + public static bool EspLocal, EspPlayers = true, EspHostile = true, EspCritters, EspTown, EspBossRainbow = true, EspItems, EspProjHostile, EspProjFriendly, EspWaypoints = true; + public static EspBoxStyle EspStyle; + public static float EspThickness = 1f, EspMaxDistance; // tiles, 0 = unlimited + public static Color ColLocal = Color.White, ColPlayers = new(70, 200, 255), ColHostile = new(235, 60, 60), ColCritters = new(200, 170, 120), ColTown = new(80, 220, 110), ColBoss = new(255, 120, 40), ColItems = new(255, 210, 70), ColProjHostile = new(255, 80, 200), ColProjFriendly = new(120, 160, 255), ColWaypoint = new(220, 90, 255); + public static bool Spelunker, Hunter, Shine, NightOwl, DangerSense, BiomeSight; + + public static bool FreezeTime; + + public static bool LogBallistics = true; + + public static readonly Dictionary Binds = []; + + static readonly FieldInfo[] Fields = typeof(Settings).GetFields(BindingFlags.Public | BindingFlags.Static); + static readonly Dictionary Defaults = []; + static string? _lastSaved; + static int _tick; + + static string FilePath => Path.Combine(Main.SavePath, "TerraSharp.json"); + + static Settings() + { + foreach (FieldInfo field in Fields) + if (!field.IsInitOnly) + Defaults[field.Name] = field.GetValue(null)!; + } + + public static object? Default(string field) => Defaults.TryGetValue(field, out object? value) ? value : null; + + static string Serialize() + { + var document = new Dictionary(); + foreach (FieldInfo field in Fields) + { + if (field.IsInitOnly) + continue; + + object value = field.GetValue(null) switch + { + Color color => Hex(color), + Enum option => option.ToString(), + var setting => setting!, + }; + SetValue(document, Paths[field.Name], value); + } + + var binds = new Dictionary(); + foreach (KeyValuePair bind in Binds) + binds[bind.Key] = bind.Value.ToString(); + document["Binds"] = binds; + + var windows = new Dictionary(); + foreach (Window window in WindowManager.All) + windows[window.Id] = [(int)window.Pos.X, (int)window.Pos.Y]; + document["Windows"] = windows; + + document["Waypoints"] = Waypoints.Save(); + + var models = new Dictionary(); + foreach (KeyValuePair model in Ballistics.Save()) + models[model.Key.ToString(CultureInfo.InvariantCulture)] = model.Value; + SetValue(document, "Ballistics.Models", models); + + return Json.Serialize(document); + } + + public static void Load() + { + Binds.Clear(); + Binds["Menu"] = Keys.Insert; + try + { + if (!File.Exists(FilePath)) + return; + + var document = Json.Parse(File.ReadAllText(FilePath)) as Dictionary ?? throw new FormatException("expected a JSON object"); + foreach (KeyValuePair setting in Paths) + if (TryGetValue(document, setting.Value, out object? value)) + TryLoad(setting.Value, () => Apply(setting.Key, Convert.ToString(value, CultureInfo.InvariantCulture) ?? "")); + + foreach (KeyValuePair bind in Section(document, "Binds")) + TryLoad("Binds." + bind.Key, () => Binds[bind.Key] = (Keys)Enum.Parse(typeof(Keys), (string)bind.Value)); + + foreach (KeyValuePair window in Section(document, "Windows")) + { + TryLoad("Windows." + window.Key, () => + { + int[] position = Json.ConvertTo(window.Value); + foreach (Window target in WindowManager.All) + if (target.Id == window.Key) + target.Pos = new Vector2(position[0], position[1]); + }); + } + + foreach (KeyValuePair world in Section(document, "Waypoints")) + TryLoad("Waypoints." + world.Key, () => Waypoints.Load(world.Key, Json.ConvertTo>(world.Value))); + + foreach (KeyValuePair model in Section(document, "Ballistics.Models")) + TryLoad("Ballistics.Models." + model.Key, () => Ballistics.Load(int.Parse(model.Key, CultureInfo.InvariantCulture), Json.ConvertTo(model.Value))); + + _lastSaved = Serialize(); + } + catch (Exception e) + { + Log.Error("settings load failed", e); + } + } + + static void TryLoad(string key, Action load) + { + try + { + load(); + } + catch (Exception e) + { + Log.Warning($"settings: bad entry '{key}': {e.Message}"); + } + } + + static void Apply(string key, string value) + { + FieldInfo? field = typeof(Settings).GetField(key, BindingFlags.Public | BindingFlags.Static); + if (field is null || field.IsInitOnly) + return; + + Type type = field.FieldType; + object? parsed = type switch + { + _ when type == typeof(bool) => bool.Parse(value), + _ when type == typeof(int) => int.Parse(value, CultureInfo.InvariantCulture), + _ when type == typeof(float) => float.Parse(value, CultureInfo.InvariantCulture), + _ when type == typeof(Color) => ParseHex(value), + _ when type.IsEnum => Enum.Parse(type, value), + _ => null, + }; + if (parsed is not null) + field.SetValue(null, parsed); + } + + public static void AutoSave() + { + if (++_tick % 120 == 0) + Flush(); + } + + public static void Flush() + { + try + { + string json = Serialize(); + if (json == _lastSaved) + return; + + File.WriteAllText(FilePath, json); + _lastSaved = json; + } + catch (Exception e) + { + Log.Error("settings save", e); + } + } + + public static void ResetAll() + { + foreach (KeyValuePair pair in Defaults) + typeof(Settings).GetField(pair.Key)!.SetValue(null, pair.Value); + } + + public static string Hex(Color color) => $"{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}"; + + public static Color ParseHex(string text) + { + text = text.Trim().TrimStart('#'); + if (text.Length is not (6 or 8)) + throw new FormatException("expected RRGGBB or RRGGBBAA"); + + uint packed = uint.Parse(text, NumberStyles.HexNumber); + return text.Length == 6 + ? new Color((int)(packed >> 16 & 255), (int)(packed >> 8 & 255), (int)(packed & 255)) + : new Color((int)(packed >> 24 & 255), (int)(packed >> 16 & 255), (int)(packed >> 8 & 255), (int)(packed & 255)); + } +} diff --git a/src/TerraSharp/Entry.cs b/src/TerraSharp/Entry.cs new file mode 100644 index 0000000..6121338 --- /dev/null +++ b/src/TerraSharp/Entry.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.CompilerServices; +using TerraSharp.Core; + +namespace TerraSharp; + +// Native entry point for ExecuteInDefaultAppDomain and hostfxr get_function_pointer. +public static class Entry +{ +#if TMODLOADER + public static int Init(IntPtr argument, int size) => Run(); +#else + static readonly Dictionary Dependencies = new(StringComparer.Ordinal); + + public static int Init(string argument) + { + AppDomain.CurrentDomain.AssemblyResolve += ResolveEmbedded; + return Run(); + } +#endif + + static int Run() + { + try + { + Log.Info("Init " + typeof(Entry).Assembly.FullName + " from " + typeof(Entry).Assembly.Location); + Load(); + return 0; + } + catch (Exception e) + { + Log.Error("Init failed", e); + return -1; + } + } + + // Dependencies must resolve before MonoMod types are JIT-compiled. + [MethodImpl(MethodImplOptions.NoInlining)] + static void Load() => Plugin.Load(); +#if !TMODLOADER + + internal static Assembly? ResolveEmbedded(object? sender, ResolveEventArgs args) + { + string? name = new AssemblyName(args.Name).Name; + if (name is null) + return null; + + lock (Dependencies) + { + if (Dependencies.TryGetValue(name, out Assembly? assembly)) + return assembly; + + using Stream? stream = typeof(Entry).Assembly.GetManifestResourceStream("Dependencies." + name + ".dll"); + if (stream is null) + return null; + + foreach (Assembly loaded in AppDomain.CurrentDomain.GetAssemblies()) + if (loaded.GetName().Name == name) + return Dependencies[name] = loaded; + + using var reader = new BinaryReader(stream); + return Dependencies[name] = Assembly.Load(reader.ReadBytes(checked((int)stream.Length))); + } + } +#endif +} diff --git a/src/TerraSharp/Features/AutoFishing.cs b/src/TerraSharp/Features/AutoFishing.cs new file mode 100644 index 0000000..cecf4e9 --- /dev/null +++ b/src/TerraSharp/Features/AutoFishing.cs @@ -0,0 +1,58 @@ +using Microsoft.Xna.Framework; +using Terraria; +using TerraSharp.Core; +using TerraSharp.Features.Combat; +using TerraSharp.UI; + +namespace TerraSharp.Features; + +// A bite is a bobber with ai[1] < 0; localAI[1] holds the catch (item type, or -npc type for enemy catches). +public static class AutoFishing +{ + static Vector2? _castAt; + static int _cooldown; + + // Player.ItemCheckWrapped prefix: controls are final, ItemCheck is about to run. + public static void PreItemCheck(Player player) + { + if (!Settings.AutoFish || player.HeldItem.fishingPole <= 0 || player.dead || WindowManager.MouseOver) + return; + + if (_cooldown > 0) + _cooldown--; + + bool any = false, bite = false; + for (int i = 0; i < Main.maxProjectiles; i++) + { + Projectile projectile = Main.projectile[i]; + if (!projectile.active || !projectile.bobber || projectile.owner != Main.myPlayer) + continue; + + any = true; + bite |= projectile.ai[0] == 0f && projectile.ai[1] < 0f && (projectile.localAI[1] > 0f || Settings.FishEnemies); + } + + if (!any && player.controlUseItem && player.releaseUseItem) + _castAt = Main.MouseWorld; + + if (player.itemAnimation > 0 || _cooldown > 0) + return; + + if (bite) + { + Use(player); + } + else if (!any && Settings.AutoRecast && _castAt is { } castAt) + { + AimOverride.Set(castAt); + Use(player); + } + } + + static void Use(Player player) + { + player.controlUseItem = true; + player.releaseUseItem = true; + _cooldown = 20; + } +} diff --git a/src/TerraSharp/Features/Combat/AimOverride.cs b/src/TerraSharp/Features/Combat/AimOverride.cs new file mode 100644 index 0000000..da5d2d6 --- /dev/null +++ b/src/TerraSharp/Features/Combat/AimOverride.cs @@ -0,0 +1,24 @@ +using Microsoft.Xna.Framework; +using TerraSharp.Core; + +namespace TerraSharp.Features.Combat; + +// Restored in the ItemCheckWrapped cleanup. +public static class AimOverride +{ + static (int X, int Y)? _saved; + + public static void Set(Vector2 world) + { + _saved ??= GameMouse.Save(); + GameMouse.PointAt(world); + } + + public static void Restore() + { + if (_saved is not { } saved) + return; + GameMouse.Restore(saved); + _saved = null; + } +} diff --git a/src/TerraSharp/Features/Combat/Aimbot.cs b/src/TerraSharp/Features/Combat/Aimbot.cs new file mode 100644 index 0000000..1c189dd --- /dev/null +++ b/src/TerraSharp/Features/Combat/Aimbot.cs @@ -0,0 +1,438 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using Terraria; +using Terraria.ID; +#if TMODLOADER +using Terraria.ModLoader; +#endif +using TerraSharp.Core; +using TerraSharp.Simulation; +using TerraSharp.UI; + +namespace TerraSharp.Features.Combat; + +public enum ShotCheck +{ + Unsupported, + Miss, + Hit, +} + +public static class Aimbot +{ + public static NPC? Target { get; private set; } + + public static NPC? Unreachable { get; private set; } + + public static Vector2 AimPoint { get; private set; } + + public static Vector2 Origin { get; private set; } + + public static bool Firing { get; private set; } + + public static bool Verified { get; private set; } + + const int GridX = 3, GridY = 5; + const int MaxEvaluated = 3; + + static Vector2 _spot; + static int _swingDirection; + static int _correctionNpc = -1, _correctionItem; + static Vector2 _correction; + static readonly Vector2[] PlayerPath = new Vector2[90]; + static readonly Rectangle[] SwingBoxes = new Rectangle[90], BackSwingBoxes = new Rectangle[90]; + static readonly Vector2[] Flight = new Vector2[151]; + static readonly Vector2[] NpcOffsets = new Vector2[151]; + static readonly bool[,] Reachable = new bool[GridX, GridY]; + static readonly Vector2[,] Directions = new Vector2[GridX, GridY]; + + static bool IsWeapon(Item item) => item is { damage: > 0, pick: 0, axe: 0, hammer: 0, fishingPole: 0, createTile: < 0, createWall: < 0 }; + + static bool Shoots(Item item) => IsWeapon(item) && item is { shoot: > 0, shootSpeed: > 0f }; + + static bool Swings(Item item) => IsWeapon(item) && item is { useStyle: 1, noMelee: false }; + + static bool Stabs(Item item) => Shoots(item) && !item.channel && ContentSamples.ProjectilesByType.TryGetValue(item.shoot, out Projectile? sample) && Sandbox.Stabs(sample); + + // Raw mouse in backbuffer pixels; Main.mouseX during the update tick depends on the last applied zoom. + static Vector2 Cursor + { + get + { + MouseState mouseState = Mouse.GetState(); + return new Vector2(mouseState.X, mouseState.Y); + } + } + + static List Candidates(Player player, bool useFov, NPC? previousTarget) + { + Vector2 cursor = Cursor; + float range = Settings.AimRange * 16f; + var scored = new List<(NPC Npc, float Score)>(); + + for (int i = 0; i < Main.maxNPCs; i++) + { + NPC npc = Main.npc[i]; + if (!Hostility.IsDamageable(npc) || Vector2.Distance(npc.Center, player.Center) > range) + continue; + + float distance = Vector2.Distance(Render.ToScreen(npc.Center), cursor); + if (useFov && Settings.AimFov > 0f && distance > Settings.AimFov && npc != previousTarget) + continue; + + scored.Add((npc, distance - (npc == previousTarget ? 80f : 0f))); + } + + scored.Sort((a, b) => a.Score.CompareTo(b.Score)); + List result = new(Math.Min(8, scored.Count)); + for (int i = 0; i < result.Capacity; i++) + result.Add(scored[i].Npc); + + return result; + } + + static bool BestSpot(NPC npc, int projectileType, float speed, out Vector2 spot, out Vector2 direction) + { + float cellWidth = npc.width / (float)GridX, cellHeight = npc.height / (float)GridY; + float tolerance = Math.Max(4f, Math.Min(cellWidth, cellHeight) * 0.6f); + + Vector2 Cell(int x, int y) => npc.position + new Vector2(cellWidth * (x + 0.5f), cellHeight * (y + 0.5f)); + + bool hasReachableSpot = false; + for (int x = 0; x < GridX; x++) + for (int y = 0; y < GridY; y++) + { + Reachable[x, y] = Ballistics.Solve(projectileType, speed, Origin, Cell(x, y), Settings.AimPrediction ? npc : null, tolerance, out Directions[x, y]); + hasReachableSpot |= Reachable[x, y]; + } + + spot = npc.Center; + direction = Vector2.Zero; + if (!hasReachableSpot) + return false; + + float bestScore = float.MinValue; + for (int x = 0; x < GridX; x++) + for (int y = 0; y < GridY; y++) + { + if (!Reachable[x, y]) + continue; + + int depth = Math.Min(Math.Min(x + 1, GridX - x), Math.Min(y + 1, GridY - y)); + for (int blockedX = 0; blockedX < GridX; blockedX++) + for (int blockedY = 0; blockedY < GridY; blockedY++) + if (!Reachable[blockedX, blockedY]) + depth = Math.Min(depth, Math.Max(Math.Abs(blockedX - x), Math.Abs(blockedY - y))); + + float score = depth * 10f - (Math.Abs(x - (GridX - 1) / 2f) + Math.Abs(y - (GridY - 1) / 2f)); + if (score <= bestScore) + continue; + + bestScore = score; + spot = Cell(x, y); + direction = Directions[x, y]; + } + + return true; + } + + static bool ShotInfo(Player player, Item item, out int projectileType, out float speed) + { + projectileType = item.shoot; + speed = item.shootSpeed; +#if TMODLOADER + if (item.useAmmo > 0 && !player.PickAmmo(item, out projectileType, out speed, out _, out _, out _, true)) + return false; + if (item.CountsAsClass(DamageClass.Melee) && !ProjectileID.Sets.NoMeleeSpeedVelocityScaling[projectileType]) + speed *= player.GetTotalAttackSpeed(DamageClass.Melee); +#else + if (item.useAmmo > 0) + { + bool canShoot = false; + int damage = item.damage; + float knockback = item.knockBack; + player.PickAmmo(item, ref projectileType, ref speed, ref canShoot, ref damage, ref knockback, out _, true); + if (!canShoot) + return false; + } + if (item.melee && !ProjectileID.Sets.NoMeleeSpeedVelocityScaling[projectileType]) + speed /= player.meleeSpeed; +#endif + return projectileType > 0 && speed > 0f && !float.IsInfinity(speed); + } + + // Player.CanHit with both entities moved to their predicted positions. + static bool CanHit(Player player, int direction, Vector2 playerPosition, NPC npc, Vector2 npcPosition) + { + Vector2 center = playerPosition + new Vector2(player.width / 2f, player.height / 2f), npcCenter = npcPosition + new Vector2(npc.width / 2f, npc.height / 2f); + Vector2 shoulder = center + new Vector2(direction * player.width / 2, player.gravDir * -player.height / 3f); + return Collision.CanHit(playerPosition, player.width, player.height, npcPosition, npc.width, npc.height) || Collision.CanHitLine(shoulder, 0, 0, npcCenter + new Vector2(0f, -npc.height / 3f), 0, 0) || Collision.CanHitLine(shoulder, 0, 0, npcCenter, 0, 0) || Collision.CanHitLine(center + new Vector2(direction * player.width / 2, 0f), 0, 0, npcCenter + new Vector2(0f, npc.height / 3), 0, 0); + } + + // First swing tick that hits the NPC, following ItemCheck_MeleeHitNPCs. + static int SwingHit(Player player, int direction, Rectangle[] boxes, NPC npc, int count, out Vector2 future) + { + future = npc.Center; + for (int t = npc.immune[player.whoAmI]; t < count; t++) + { + if (boxes[t].IsEmpty) + continue; + + Vector2 position = npc.position + (Settings.AimPrediction ? NpcMotion.Offset(npc, t) : Vector2.Zero); + if (!boxes[t].Intersects(new Rectangle((int)position.X, (int)position.Y, npc.width, npc.height))) + continue; + if (!npc.noTileCollide && !CanHit(player, direction, PlayerPath[t], npc, position)) + continue; + + future = position + new Vector2(npc.width / 2f, npc.height / 2f); + return t; + } + + return -1; + } + + public static void PreItemCheck(Player player) + { + NPC? previousTarget = Target; + Target = null; + Unreachable = null; + Firing = false; + Verified = false; + Ballistics.RecordShot(0, 0f); + + if (player.dead || player.CCed || player.noItems) + return; + + Item item = player.HeldItem; + bool shoots = Shoots(item), stabs = Stabs(item); + int projectileType = 0; + float speed = 0f; + bool hasShot = shoots && !stabs && ShotInfo(player, item, out projectileType, out speed); + bool attacking = player.controlUseItem || player.itemAnimation > 0; + if (hasShot && attacking) + Ballistics.RecordShot(projectileType, speed); + + if (WindowManager.MouseOver || !(Settings.Aimbot || Settings.AutoAttack)) + return; + + using var scope = Profiler.Measure(Settings.AutoAttack ? Feature.AutoAttack : Feature.Aimbot); + + bool swings = Swings(item); + if (!hasShot && !(stabs || (swings && Settings.AutoAttack))) + return; + + Origin = player.RotatedRelativePoint(player.MountedCenter); + + List candidates = Candidates(player, Settings.Aimbot && (hasShot || stabs) && !Settings.AutoAttack, previousTarget); + if (!PickTarget(player, item, candidates, previousTarget, hasShot, stabs, projectileType, speed)) + { + Unreachable = candidates.FirstOrDefault(); + return; + } + + ShotCheck check = stabs || !hasShot ? ShotCheck.Hit : ShotCheck.Unsupported; + Verified = check == ShotCheck.Hit; + if (hasShot && Settings.AimExact && (attacking || Settings.AutoAttack)) + { + Vector2 aim = AimPoint; + check = Refine(player, item, Target!, _spot, ref aim); + if (check != ShotCheck.Unsupported) + { + AimPoint = aim; + Verified = check == ShotCheck.Hit; + } + } + + if (!hasShot && !stabs && Settings.AutoAttack && player.itemAnimation == 0) + player.direction = _swingDirection; + if (Settings.AutoAttack && check != ShotCheck.Miss) + Press(player, item); + + if (hasShot && (player.controlUseItem || player.itemAnimation > 0)) + Ballistics.RecordShot(projectileType, speed); + + if ((player.controlUseItem || player.itemAnimation > 0) && (Settings.AutoAttack || (Settings.Aimbot && (hasShot || stabs)))) + { + AimOverride.Set(AimPoint); + Firing = true; + } + } + + // Tries the other facing too, since it can still change before the swing starts. + static bool PickSwingTarget(Player player, Item item, List candidates, NPC? previousTarget) + { + if (candidates.Count == 0) + return false; + + // A swing in progress can't be redirected. + if (player.itemAnimation > 0) + { + if (previousTarget is null || !candidates.Contains(previousTarget)) + return false; + + Target = previousTarget; + AimPoint = previousTarget.Center; + + return true; + } + + int ticks = Math.Min(PlayerPath.Length, item.useAnimation * 2 + 1); + int count = Sandbox.Swing(player, item, Settings.AimPrediction, PlayerPath, ticks, SwingBoxes, BackSwingBoxes); + if (count <= 0) + return false; + + for (int pass = 0; pass < 2; pass++) + { + int direction = pass == 0 ? player.direction : -player.direction; + foreach (NPC npc in candidates) + { + if (SwingHit(player, direction, pass == 0 ? SwingBoxes : BackSwingBoxes, npc, count, out Vector2 future) < 0) + continue; + + Target = npc; + AimPoint = future; + _swingDirection = direction; + + return true; + } + } + + return false; + } + + static bool PickTarget(Player player, Item item, List candidates, NPC? previousTarget, bool hasShot, bool stabs, int projectileType, float speed) + { + if (!hasShot && !stabs) + return PickSwingTarget(player, item, candidates, previousTarget); + + int evaluated = 0; + foreach (NPC npc in candidates) + { + if (stabs) + { + if (++evaluated > MaxEvaluated) + return false; + + Vector2 aim = npc.Center; + if (Refine(player, item, npc, npc.Center, ref aim) != ShotCheck.Hit) + continue; + + Target = npc; + _spot = npc.Center; + AimPoint = aim; + + return true; + } + + if (++evaluated > MaxEvaluated) + return false; + if (!BestSpot(npc, projectileType, speed, out Vector2 spot, out Vector2 direction)) + continue; + + Target = npc; + _spot = spot; + AimPoint = Origin + direction * Math.Max(64f, Vector2.Distance(Origin, spot)); + + return true; + } + + return false; + } + + // Starts from the correction that hit this NPC with this item last time. + static ShotCheck Refine(Player player, Item item, NPC npc, Vector2 spot, ref Vector2 aim) + { + for (int t = 0; t < NpcOffsets.Length; t++) + NpcOffsets[t] = Settings.AimPrediction ? NpcMotion.Offset(npc, t) : Vector2.Zero; + + Vector2 start = aim; + if (_correctionNpc == npc.whoAmI && _correctionItem == item.type) + aim += _correction; + + Vector2 bestAim = aim; + float bestMiss = float.MaxValue; + ShotCheck result = ShotCheck.Miss; + + for (int round = 0; round < 3; round++) + { + List? shots = Sandbox.DryRun(player, item, aim); + if (shots is null) + { + if (round == 0) + result = ShotCheck.Unsupported; + break; + } + + bool hit = false; + float miss = float.MaxValue; + Vector2 correction = Vector2.Zero; + + for (int shotIndex = 0; shotIndex < shots.Count && shotIndex < 3 && !hit; shotIndex++) + { + Projectile projectile = shots[shotIndex].Projectile; + int width = Math.Max(1, projectile.width), height = Math.Max(1, projectile.height); + bool primary = shotIndex == 0; + int ticks = Math.Min(Flight.Length - 1, (int)(Vector2.Distance(projectile.Center, spot) / Math.Max(1f, projectile.velocity.Length()) * 3f) + 30); + + Sandbox.Fly(shots[shotIndex], ticks, Flight, t => + { + Vector2 offset = NpcOffsets[t]; + var npcBounds = new Rectangle((int)(npc.position.X + offset.X), (int)(npc.position.Y + offset.Y), npc.width, npc.height); + var projectileBounds = new Rectangle((int)(Flight[t].X - width / 2f), (int)(Flight[t].Y - height / 2f), width, height); + if (npcBounds.Intersects(projectileBounds)) + return hit = true; + if (!primary) + return false; + + Vector2 goal = spot + offset; + float distance = Vector2.Distance(Flight[t], goal); + if (distance < miss) + { + miss = distance; + correction = goal - Flight[t]; + } + + return false; + }); + } + + if (hit) + { + bestAim = aim; + result = ShotCheck.Hit; + break; + } + + if (miss < bestMiss) + { + bestMiss = miss; + bestAim = aim; + } + + if (correction == Vector2.Zero) + break; + + aim += correction; + } + + aim = bestAim; + if (result == ShotCheck.Hit) + (_correctionNpc, _correctionItem, _correction) = (npc.whoAmI, item.type, aim - start); + else if (_correctionNpc == npc.whoAmI) + _correctionNpc = -1; + + return result; + } + + static void Press(Player player, Item item) + { + if (player.itemAnimation > 0 || (item.mana > 0 && player.statMana < item.mana && !Settings.NoManaCost)) + return; + + player.controlUseItem = true; + player.releaseUseItem = true; + } +} diff --git a/src/TerraSharp/Features/Combat/CombatFeatures.cs b/src/TerraSharp/Features/Combat/CombatFeatures.cs new file mode 100644 index 0000000..c4dc015 --- /dev/null +++ b/src/TerraSharp/Features/Combat/CombatFeatures.cs @@ -0,0 +1,102 @@ +using System; +using Microsoft.Xna.Framework; +using Terraria; +using TerraSharp.Core; + +namespace TerraSharp.Features.Combat; + +public static class CombatFeatures +{ + static int _auraTick; + + static float RangeSq(float tiles) => tiles * 16f * tiles * 16f; + + // NPC.AI postfix: after the AI chose its velocity, before UpdateNPC applies it. + public static void PostNpcAI(NPC npc) + { + if (!(Settings.FreezeEnemies || Settings.WeakenEnemies) || Main.gameMenu || !Hostility.IsHostile(npc)) + return; + + if (Settings.FreezeEnemies) + npc.velocity = Vector2.Zero; + if (Settings.WeakenEnemies && npc.life > 1) + npc.life = 1; + } + + public static void PostPlayerUpdate(Player player) + { + if (player.dead) + return; + + if (Settings.KillAura && ++_auraTick >= Settings.KillAuraInterval) + { + _auraTick = 0; + float range = RangeSq(Settings.KillAuraRange); + for (int i = 0; i < Main.maxNPCs; i++) + { + NPC npc = Main.npc[i]; + if (Hostility.IsDamageable(npc) && Vector2.DistanceSquared(npc.Center, player.Center) <= range) + Kill(npc, player); + } + } + + if (Settings.HitMagnet && player.itemAnimation > 0 && Nearest(player, RangeSq(Settings.HitMagnetRange)) is NPC target) + { + target.Center = player.Center + new Vector2(player.direction * (player.width / 2f + target.width / 2f), 0f); + target.velocity = Vector2.Zero; + } + } + + static NPC? Nearest(Player player, float maxDistanceSq) + { + NPC? best = null; + float bestDistance = maxDistanceSq; + for (int i = 0; i < Main.maxNPCs; i++) + { + NPC npc = Main.npc[i]; + if (!Hostility.IsDamageable(npc)) + continue; + + float distance = Vector2.DistanceSquared(npc.Center, player.Center); + if (distance < bestDistance) + { + bestDistance = distance; + best = npc; + } + } + + return best; + } + + static void Kill(NPC npc, Player player) + { + int damage = Math.Max(npc.life, npc.lifeMax) * 4 + npc.defense * 2 + 1000; +#if TMODLOADER + player.ApplyDamageToNPC(npc, damage, 0f, npc.Center.X < player.Center.X ? -1 : 1, false, player.HeldItem.DamageType); +#else + player.ApplyDamageToNPC(npc, damage, 0f, npc.Center.X < player.Center.X ? -1 : 1, false, null, player.HeldItem.type, 0); +#endif + } + + public static void GatherAll() + { + Player player = Main.LocalPlayer; + for (int i = 0; i < Main.maxNPCs; i++) + { + NPC npc = Main.npc[i]; + if (!Hostility.IsHostile(npc)) + continue; + + npc.Center = player.Center; + npc.velocity = Vector2.Zero; + } + } + + public static void KillAll() + { + Player player = Main.LocalPlayer; + for (int i = 0; i < Main.maxNPCs; i++) + if (Hostility.IsDamageable(Main.npc[i])) + Kill(Main.npc[i], player); + } +} diff --git a/src/TerraSharp/Features/Combat/Hostility.cs b/src/TerraSharp/Features/Combat/Hostility.cs new file mode 100644 index 0000000..2892ded --- /dev/null +++ b/src/TerraSharp/Features/Combat/Hostility.cs @@ -0,0 +1,12 @@ +using Terraria; + +namespace TerraSharp.Features.Combat; + +public static class Hostility +{ + public static bool IsHostile(NPC npc) => npc.active && !npc.friendly && !npc.townNPC && !npc.CountsAsACritter && !npc.immortal && npc.lifeMax > 5; + + public static bool IsCritter(NPC npc) => npc.active && !npc.townNPC && (npc.CountsAsACritter || (!npc.friendly && npc.lifeMax <= 5 && npc.damage == 0)); + + public static bool IsDamageable(NPC npc) => IsHostile(npc) && !npc.dontTakeDamage; +} diff --git a/src/TerraSharp/Features/Combat/NpcMotion.cs b/src/TerraSharp/Features/Combat/NpcMotion.cs new file mode 100644 index 0000000..f7a0e53 --- /dev/null +++ b/src/TerraSharp/Features/Combat/NpcMotion.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Terraria; +using Terraria.ID; +using TerraSharp.Core; +using TerraSharp.Simulation; + +namespace TerraSharp.Features.Combat; + +public static class NpcMotion +{ + const int Horizon = 180; + const int MaxReuse = 30; + const int SimulationsPerTick = 4; + + static readonly Vector2[] LastVelocities = new Vector2[Main.maxNPCs + 1]; + static readonly Vector2[] Accelerations = new Vector2[Main.maxNPCs + 1]; + static readonly int[] SeenTypes = new int[Main.maxNPCs + 1]; + + static readonly Vector2[][] Paths = [.. Enumerable.Range(0, Main.maxNPCs + 1).Select(_ => new Vector2[Horizon + 1])]; + static readonly Vector2[][] Velocities = [.. Enumerable.Range(0, Main.maxNPCs + 1).Select(_ => new Vector2[Horizon + 1])]; + static readonly int[] Counts = new int[Main.maxNPCs + 1]; + static readonly int[] SimulatedTicks = [.. Enumerable.Repeat(-1, Main.maxNPCs + 1)]; + static readonly int[] SimulatedTypes = new int[Main.maxNPCs + 1]; + static readonly int[] Signatures = new int[Main.maxNPCs + 1]; + static int _budgetTick = -1, _budget; + static int _signatureTick = -1, _signature; + + sealed class Incoming(Projectile projectile, Vector2[] path, int from, int count, int immunity) + { + public readonly Vector2[] Path = path; + public readonly int From = from, Count = count, Width = Math.Max(1, projectile.width), Height = Math.Max(1, projectile.height); + public readonly int Owner = projectile.owner, Damage = projectile.damage, Direction = projectile.direction != 0 ? projectile.direction : Math.Sign(projectile.velocity.X); + public readonly float Knockback = projectile.knockBack; + public readonly bool Local = projectile.usesLocalNPCImmunity || projectile.usesIDStaticNPCImmunity; + public readonly int Cooldown = projectile.usesLocalNPCImmunity ? projectile.localNPCHitCooldown : projectile.idStaticNPCHitCooldown; + public int Immunity = immunity, Penetrate = projectile.penetrate; + } + + static readonly List Projectiles = []; + + public static void Track() + { + for (int i = 0; i < Main.maxNPCs; i++) + { + NPC npc = Main.npc[i]; + if (!npc.active || SeenTypes[i] != npc.type) + { + SeenTypes[i] = npc.active ? npc.type : -1; + LastVelocities[i] = npc.velocity; + Accelerations[i] = Vector2.Zero; + continue; + } + + Vector2 acceleration = npc.velocity - LastVelocities[i]; + LastVelocities[i] = npc.velocity; + + // Hits, teleports and landings show up as spikes. + if (acceleration.Length() > 2f) + acceleration = Vector2.Zero; + + var clamped = new Vector2(MathHelper.Clamp(acceleration.X, -0.5f, 0.5f), MathHelper.Clamp(acceleration.Y, -0.5f, 0.5f)); + Accelerations[i] = Vector2.Lerp(Accelerations[i], clamped, 0.3f); + } + } + + // Displacement of npc.position after the given number of ticks. + public static Vector2 Offset(NPC npc, int ticks) + { + int count = Simulate(npc, out int from); + if (count - from < 2) + return Extrapolate(npc.velocity, Accelerations[npc.whoAmI], ticks); + + Vector2[] path = Paths[npc.whoAmI]; + int index = from + Math.Max(0, ticks); + if (index < count) + return path[index] - path[from]; + + Vector2 last = path[count - 1]; + return last + (last - path[count - 2]) * (index - count + 1) - path[from]; + } + + // Runs the NPC's own AI; a path stays in use while the NPC keeps following it. Acceleration extrapolation is the fallback. + static int Simulate(NPC npc, out int from) + { + int i = npc.whoAmI; + from = Clock.Tick - SimulatedTicks[i]; + if (SimulatedTypes[i] == npc.type && from == 0) + return Counts[i]; + if (SimulatedTypes[i] == npc.type && from > 0 && from <= MaxReuse && Follows(npc, from)) + return Counts[i]; + + from = 0; + if (_budgetTick != Clock.Tick) + (_budgetTick, _budget) = (Clock.Tick, SimulationsPerTick); + if (_budget <= 0 || !Sandbox.CanMove(npc)) + return 0; + + _budget--; + using var scope = Profiler.Measure(Feature.NpcMotion); + (SimulatedTicks[i], SimulatedTypes[i], Signatures[i]) = (Clock.Tick, npc.type, Signature(npc)); + CollectProjectiles(npc); + Counts[i] = Sandbox.Move(npc, Horizon, Paths[i], Velocities[i], Projectiles.Count > 0 ? ApplyHits : null); + + return Counts[i]; + } + + // The game's random rolls, hits and AI changes all show up as the NPC leaving its recorded path. + static bool Follows(NPC npc, int from) + { + int i = npc.whoAmI; + if (from >= Counts[i] - 1 || Signatures[i] != Signature(npc)) + return false; + + return Vector2.DistanceSquared(npc.position, Paths[i][from]) < 1f && Vector2.DistanceSquared(npc.velocity, Velocities[i][from]) < 0.01f; + } + + // Changes when friendly projectiles that could knock the NPC back appear or disappear. + static int Signature(NPC npc) + { + if (npc.knockBackResist <= 0f || npc.dontTakeDamage) + return 0; + if (_signatureTick == Clock.Tick) + return _signature; + + int signature = 17; + for (int i = 0; i < Main.maxProjectiles; i++) + { + Projectile projectile = Main.projectile[i]; + if (projectile.active && projectile.friendly && !projectile.hostile && projectile.damage > 0 && projectile.knockBack > 0f) + signature = (signature * 31 + i) * 31 + projectile.type; + } + + (_signatureTick, _signature) = (Clock.Tick, signature); + return signature; + } + + // Friendly projectiles in flight, from the cached sandbox paths the ESP also draws. + static void CollectProjectiles(NPC npc) + { + Projectiles.Clear(); + if (npc.knockBackResist <= 0f || npc.dontTakeDamage) + return; + + for (int i = 0; i < Main.maxProjectiles; i++) + { + Projectile projectile = Main.projectile[i]; + if (!projectile.active || !projectile.friendly || projectile.hostile || projectile.npcProj || projectile.damage <= 0 || projectile.knockBack <= 0f) + continue; + + int count = Sandbox.LivePath(projectile, i, Clock.Tick, out Vector2[] path, out int from); + if (count - from < 2) + continue; + + int immunity = projectile.usesLocalNPCImmunity ? projectile.localNPCImmunity[npc.whoAmI] : 0; + Projectiles.Add(new Incoming(projectile, path, from, count, immunity)); + } + } + + // Hit rules follow Projectile.Damage: owner immunity of 10 ticks unless the projectile tracks its own. + static void ApplyHits(NPC npc, int tick) + { + foreach (Incoming incoming in Projectiles) + { + if (incoming.Immunity > 0) + incoming.Immunity--; + + int index = incoming.From + tick; + if (incoming.Penetrate == 0 || index >= incoming.Count) + continue; + + Vector2 center = incoming.Path[index]; + var bounds = new Rectangle((int)(center.X - incoming.Width / 2f), (int)(center.Y - incoming.Height / 2f), incoming.Width, incoming.Height); + if (!bounds.Intersects(npc.Hitbox) || (incoming.Local ? incoming.Immunity != 0 : npc.immune[incoming.Owner] > 0)) + continue; + + int direction = incoming.Direction != 0 ? incoming.Direction : Math.Sign(npc.Center.X - center.X); + Knockback(npc, incoming.Knockback, incoming.Damage, direction); + + if (incoming.Local) + incoming.Immunity = incoming.Cooldown; + else + npc.immune[incoming.Owner] = 10; + if (incoming.Penetrate > 0) + incoming.Penetrate--; + } + } + + // Knockback part of NPC.StrikeNPC, the same on vanilla and tModLoader. + static void Knockback(NPC npc, float knockback, int damage, int hitDirection) + { + float strength = knockback * npc.knockBackResist * (npc.onFire2 ? 1.1f : 1f); + if (strength > 8f) + strength = 8f + (strength - 8f) * 0.9f; + if (strength > 10f) + strength = 10f + (strength - 10f) * 0.8f; + if (strength > 12f) + strength = 12f + (strength - 12f) * 0.7f; + if (strength > 14f) + strength = 14f + (strength - 14f) * 0.6f; + strength = Math.Min(strength, 16f); + + int taken = Math.Max(1, (int)(damage - npc.defense * 0.5)); + npc.justHit = true; + + if (taken * (Main.expertMode ? 15 : 10) <= npc.lifeMax) + { + npc.velocity.Y = -strength * (npc.noGravity ? 0.5f : 0.75f) * npc.knockBackResist; + npc.velocity.X = strength * hitDirection * npc.knockBackResist; + return; + } + + if (hitDirection < 0 && npc.velocity.X > -strength) + { + if (npc.velocity.X > 0f) + npc.velocity.X -= strength; + npc.velocity.X = Math.Max(npc.velocity.X - strength, -strength); + } + else if (hitDirection > 0 && npc.velocity.X < strength) + { + if (npc.velocity.X < 0f) + npc.velocity.X += strength; + npc.velocity.X = Math.Min(npc.velocity.X + strength, strength); + } + + if (npc.type == NPCID.SnowFlinx) + strength *= 1.5f; + float lift = strength * (npc.noGravity ? -0.5f : -0.75f); + if (npc.velocity.Y > lift) + npc.velocity.Y = Math.Max(npc.velocity.Y + lift, lift); + } + + // Acceleration extrapolation is capped at 40 ticks. + static Vector2 Extrapolate(Vector2 velocity, Vector2 acceleration, int ticks) + { + float accelerationTicks = Math.Min(ticks, 40); + return velocity * ticks + acceleration * (0.5f * accelerationTicks * accelerationTicks); + } +} diff --git a/src/TerraSharp/Features/Combat/ShotPreview.cs b/src/TerraSharp/Features/Combat/ShotPreview.cs new file mode 100644 index 0000000..fda48f5 --- /dev/null +++ b/src/TerraSharp/Features/Combat/ShotPreview.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Terraria; +using TerraSharp.Core; +using TerraSharp.Simulation; +using TerraSharp.UI; + +namespace TerraSharp.Features.Combat; + +public static class ShotPreview +{ + public sealed record Path(Vector2[] Points, int Count); + + public static List Paths { get; } = []; + + static int _lastTick; + + // Runs after the aim override in the ItemCheckWrapped prefix. + public static void Update(Player player) + { + if (!Settings.ShotPreview || player.dead || WindowManager.MouseOver) + { + Paths.Clear(); + return; + } + if (Clock.Tick - _lastTick < 3) + return; + _lastTick = Clock.Tick; + using var scope = Profiler.Measure(Feature.ShotPreview); + Paths.Clear(); + Item item = player.HeldItem; + if (!Sandbox.CanDryRun(item) || Sandbox.DryRun(player, item, Main.MouseWorld) is not { } shots) + return; + for (int k = 0; k < shots.Count && k < 5; k++) + { + var points = new Vector2[181]; + int count = Sandbox.Fly(shots[k], 180, points); + if (count > 1) + Paths.Add(new Path(points, count)); + } + } +} diff --git a/src/TerraSharp/Features/HeldItemOverrides.cs b/src/TerraSharp/Features/HeldItemOverrides.cs new file mode 100644 index 0000000..b229e95 --- /dev/null +++ b/src/TerraSharp/Features/HeldItemOverrides.cs @@ -0,0 +1,64 @@ +using System; +using Terraria; +using TerraSharp.Core; + +namespace TerraSharp.Features; + +public static class HeldItemOverrides +{ + const int Unbreakable = 9999; + + static Item? _item; + static int _type, _damage, _useTime, _useAnimation, _pick, _axe, _hammer; + + public static void Update(Player player) + { + bool any = Settings.DamageBoost || Settings.RapidAttack || Settings.InstantBreak; + Item? held = any && player.HeldItem.type != 0 ? player.HeldItem : null; + if (held != _item || (held is not null && held.type != _type)) + { + Restore(); + if (held is not null) + Capture(held); + } + + if (_item is null) + return; + + _item.damage = Settings.DamageBoost && _damage > 0 ? (int)(_damage * Settings.DamageMult) : _damage; + _item.useTime = Settings.RapidAttack ? Math.Min(_useTime, 4) : _useTime; + _item.useAnimation = Settings.RapidAttack ? Math.Min(_useAnimation, 6) : _useAnimation; + + _item.pick = Settings.InstantBreak && _pick > 0 ? Unbreakable : _pick; + _item.axe = Settings.InstantBreak && _axe > 0 ? Unbreakable : _axe; + _item.hammer = Settings.InstantBreak && _hammer > 0 ? Unbreakable : _hammer; + } + + static void Capture(Item item) + { + _item = item; + _type = item.type; + _damage = item.damage; + _useTime = item.useTime; + _useAnimation = item.useAnimation; + _pick = item.pick; + _axe = item.axe; + _hammer = item.hammer; + } + + public static void Restore() + { + // SetDefaults has already replaced the stats when the item type changes. + if (_item is not null && _item.type == _type) + { + _item.damage = _damage; + _item.useTime = _useTime; + _item.useAnimation = _useAnimation; + _item.pick = _pick; + _item.axe = _axe; + _item.hammer = _hammer; + } + + _item = null; + } +} diff --git a/src/TerraSharp/Features/ItemFeatures.cs b/src/TerraSharp/Features/ItemFeatures.cs new file mode 100644 index 0000000..8b8d3c8 --- /dev/null +++ b/src/TerraSharp/Features/ItemFeatures.cs @@ -0,0 +1,27 @@ +using Microsoft.Xna.Framework; +using Terraria; + +namespace TerraSharp.Features; + +public static class ItemFeatures +{ + // 1.4.5 wraps ground items in WorldItem; the removed Item.noGrabDelay no longer applies. + public static void PullAll() + { + Player player = Main.LocalPlayer; + for (int i = 0; i < Main.maxItems; i++) + { +#if TMODLOADER + Item drop = Main.item[i]; + if (!drop.active || drop.type == 0) + continue; +#else + WorldItem drop = Main.item[i]; + if (drop is not { active: true, inner.type: not 0 }) + continue; +#endif + drop.Center = player.Center; + drop.velocity = Vector2.Zero; + } + } +} diff --git a/src/TerraSharp/Features/PlayerFeatures.cs b/src/TerraSharp/Features/PlayerFeatures.cs new file mode 100644 index 0000000..7670d5a --- /dev/null +++ b/src/TerraSharp/Features/PlayerFeatures.cs @@ -0,0 +1,289 @@ +using System; +using Microsoft.Xna.Framework; +using Terraria; +using TerraSharp.Core; +using TerraSharp.UI; + +namespace TerraSharp.Features; + +public static class PlayerFeatures +{ + const int InventorySlots = 58; + + static Vector2 _flyAnchor; + static bool _godApplied, _autoSwingApplied, _autoSwingVanilla; + static readonly int[] InventoryTypes = new int[InventorySlots]; + static readonly int[] InventoryStacks = new int[InventorySlots]; + static bool _inventoryValid; + static int _healCooldown, _manaCooldown; + + public static void PreUpdate(Player player) + { + _flyAnchor = player.position; + HeldItemOverrides.Update(player); + ApplyAutoSwing(); + } + + static void ApplyAutoSwing() + { + if (Settings.AutoSwing && !_autoSwingApplied) + { + _autoSwingVanilla = Main.SettingsEnabled_AutoReuseAllItems; + _autoSwingApplied = true; + } + + if (!_autoSwingApplied) + return; + + Main.SettingsEnabled_AutoReuseAllItems = Settings.AutoSwing || _autoSwingVanilla; + if (!Settings.AutoSwing) + _autoSwingApplied = false; + } + + // Player.HorizontalMovement prefix: after ResetEffects/UpdateBuffs/UpdateEquips, before run/jump physics. + public static void PreMovement(Player player) + { + if (Settings.SuperSpeed && !Settings.Fly) + { + float top = 3f * Settings.SpeedMult; + player.maxRunSpeed = top; + player.accRunSpeed = Math.Max(player.accRunSpeed, top); + if (player.controlRight != player.controlLeft) + { + float targetSpeed = player.controlRight ? top : -top; + if (player.controlRight ? player.velocity.X < targetSpeed : player.velocity.X > targetSpeed) + player.velocity.X = targetSpeed; + } + } + + if (Settings.HighJump) + { + player.jumpSpeedBoost = 9f; + Player.jumpHeight = 45; + } + + if (Settings.NoFallDamage) + player.noFallDmg = true; + + if (Settings.LavaImmune) + { + player.lavaImmune = true; + player.fireWalk = true; + } + + if (Settings.WaterWalk) + { + player.waterWalk = true; + player.waterWalk2 = true; + if (player.wet && player.velocity.Y * player.gravDir > 0f) + player.velocity.Y = 0f; + } + + if (Settings.NoKnockback) + player.noKnockback = true; + if (Settings.GravityFlip) + player.forcedGravity = 10; + if (Settings.NoManaCost) + player.manaCost = 0f; + + if (Settings.ExtendedReach) + { + Player.tileRangeX = Settings.ReachX; + Player.tileRangeY = Settings.ReachY; + } + + ApplyVision(player); + } + + static void ApplyVision(Player player) + { + if (Settings.Spelunker) + player.findTreasure = true; + if (Settings.Hunter) + player.detectCreature = true; + if (Settings.NightOwl) + player.nightVision = true; + if (Settings.DangerSense) + player.dangerSense = true; + if (Settings.BiomeSight) + player.biomeSight = true; + if (Settings.Sonar) + player.sonarPotion = true; + if (Settings.Shine) + Lighting.AddLight((int)(player.position.X + player.width / 2f) / 16, (int)(player.position.Y + player.height / 2f) / 16, 0.8f, 0.95f, 1f); + } + + public static void PostUpdate(Player player) + { + ApplyGodMode(player); + if (!player.dead) + { + if (Settings.InfHealth && player.statLife < player.statLifeMax2) + player.statLife = player.statLifeMax2; + if (Settings.InfMana && player.statMana < player.statManaMax2) + player.statMana = player.statManaMax2; + if (Settings.InfBreath) + player.breath = player.breathMax; + AutoPotions(player); + } + + if (Settings.InfFlight) + { + if (player.wingTimeMax > 0) + player.wingTime = player.wingTimeMax; + player.rocketTime = player.rocketTimeMax; + } + + if (Settings.InstantRespawn && player.dead && player.respawnTimer > 1) + player.respawnTimer = 1; + if (Settings.Fly && !player.dead) + Fly(player); + + if (Settings.InfItems) + FreezeInventory(player); + else + _inventoryValid = false; + } + + static void ApplyGodMode(Player player) + { + if (Settings.GodMode) + { + player.creativeGodMode = true; + player.immune = true; + player.immuneNoBlink = true; + player.immuneTime = Math.Max(player.immuneTime, 30); + + int[] cooldowns = player.hurtCooldowns; + for (int i = 0; i < cooldowns.Length; i++) + cooldowns[i] = Math.Max(cooldowns[i], 30); + + _godApplied = true; + } + else if (_godApplied) + { + player.creativeGodMode = false; + player.immuneNoBlink = false; + _godApplied = false; + } + } + + // Movement starts from the position captured before vanilla physics. + static void Fly(Player player) + { + Vector2 direction = Vector2.Zero; + if (player.controlUp || player.controlJump) + direction.Y -= 1f; + if (player.controlDown) + direction.Y += 1f; + if (player.controlLeft) + direction.X -= 1f; + if (player.controlRight) + direction.X += 1f; + + player.position = _flyAnchor + direction * Settings.FlySpeed; + player.velocity = Vector2.Zero; + player.fallStart = (int)(player.position.Y / 16f); + } + + static void AutoPotions(Player player) + { + if (_healCooldown > 0) + _healCooldown--; + if (_manaCooldown > 0) + _manaCooldown--; + + if (Settings.AutoHeal && _healCooldown == 0 && player.potionDelay == 0 && player.statLife < player.statLifeMax2 * Settings.AutoHealAt / 100f) + { + player.QuickHeal(); + _healCooldown = 30; + } + + if (Settings.AutoMana && _manaCooldown == 0 && player.statMana < player.statManaMax2 * Settings.AutoManaAt / 100f) + { + player.QuickMana(); + _manaCooldown = 30; + } + } + + static void FreezeInventory(Player player) + { + int slots = Math.Min(player.inventory.Length, InventorySlots); + for (int i = 0; i < slots; i++) + { + Item item = player.inventory[i]; + if (_inventoryValid && item.type != 0 && item.type == InventoryTypes[i] && item.stack < InventoryStacks[i]) + { + item.stack = InventoryStacks[i]; + continue; + } + + InventoryTypes[i] = item.type; + InventoryStacks[i] = item.stack; + } + + _inventoryValid = true; + } + + public static void OnUnload() + { + if (_godApplied && !Main.gameMenu) + { + Main.LocalPlayer.creativeGodMode = false; + Main.LocalPlayer.immuneNoBlink = false; + } + + if (_autoSwingApplied) + Main.SettingsEnabled_AutoReuseAllItems = _autoSwingVanilla; + + _autoSwingApplied = false; + _godApplied = false; + _inventoryValid = false; + } + + public static void MaxHp() => Main.LocalPlayer.statLifeMax = 500; + + public static void MaxMana() => Main.LocalPlayer.statManaMax = 200; + + public static void FullHeal() + { + Player player = Main.LocalPlayer; + player.statLife = player.statLifeMax2; + player.statMana = player.statManaMax2; + } + + public static void AddBuff(int type) => Main.LocalPlayer.AddBuff(type, (int)(Settings.BuffMinutes * 3600f), false); + + public static void TeleportTo(Vector2 topLeft) + { + Player player = Main.LocalPlayer; + player.position = topLeft; + player.velocity = Vector2.Zero; + player.fallStart = (int)(player.position.Y / 16f); + } + + // Matches the Magic Mirror branch of Player.ItemCheck_Inner. + public static void TeleportSpawn() + { + Player player = Main.LocalPlayer; + Widgets.Sound(2, (int)player.position.X, (int)player.position.Y, 6); + MirrorDust(player, player.velocity * 0.5f); + + player.RemoveAllGrapplingHooks(); + player.Spawn(PlayerSpawnContext.RecallFromItem); + + MirrorDust(player, Vector2.Zero); + } + + static void MirrorDust(Player player, Vector2 velocity) + { + for (int i = 0; i < 70; i++) + Dust.NewDust(player.position, player.width, player.height, 15, velocity.X, velocity.Y, 150, default, 1.5f); + } + + public static void TeleportCursor() + { + Player player = Main.LocalPlayer; + TeleportTo(Main.MouseWorld - new Vector2(player.width / 2f, player.height / 2f)); + } +} diff --git a/src/TerraSharp/Features/Waypoints.cs b/src/TerraSharp/Features/Waypoints.cs new file mode 100644 index 0000000..f442886 --- /dev/null +++ b/src/TerraSharp/Features/Waypoints.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Xna.Framework; +using Terraria; + +namespace TerraSharp.Features; + +public sealed class Waypoint(string name, Vector2 position) +{ + public string Name { get; } = name; + + public Vector2 Position { get; } = position; +} + +public static class Waypoints +{ + public sealed class SavedWaypoint + { + public string Name { get; set; } = "Waypoint"; + public float X { get; set; } + public float Y { get; set; } + } + + static readonly Dictionary> ByWorld = []; + + static string WorldKey => Main.worldID.ToString(CultureInfo.InvariantCulture) + ":" + Main.worldName; + + public static List Current => ListFor(WorldKey); + + static List ListFor(string world) + { + if (!ByWorld.TryGetValue(world, out List? list)) + ByWorld[world] = list = []; + return list; + } + + public static void AddHere() + { + Player player = Main.LocalPlayer; + List list = Current; + list.Add(new Waypoint($"Waypoint {list.Count + 1} ({(int)(player.position.X / 16f)}, {(int)(player.position.Y / 16f)})", player.position)); + } + + public static Dictionary> Save() + { + var worlds = new Dictionary>(); + foreach (KeyValuePair> world in ByWorld) + worlds[world.Key] = world.Value.ConvertAll(w => new SavedWaypoint { Name = w.Name, X = w.Position.X, Y = w.Position.Y }); + return worlds; + } + + public static void Load(string world, List waypoints) + { + ByWorld[world] = waypoints.ConvertAll(w => new Waypoint(w.Name, new Vector2(w.X, w.Y))); + } +} diff --git a/src/TerraSharp/Features/WorldFeatures.cs b/src/TerraSharp/Features/WorldFeatures.cs new file mode 100644 index 0000000..0368f7e --- /dev/null +++ b/src/TerraSharp/Features/WorldFeatures.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Terraria; +using TerraSharp.Core; +using Sandstorm = Terraria.GameContent.Events.Sandstorm; + +namespace TerraSharp.Features; + +public static class WorldFeatures +{ + public static readonly IReadOnlyList MoonPhases = ["Full Moon", "Waning Gibbous", "Third Quarter", "Waning Crescent", "New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous"]; + + public static readonly IReadOnlyList<(string Name, int Id)> Invasions = [("Frost Legion", 2), ("Goblin Army", 1), ("Martian Madness", 4), ("Pirates", 3)]; + + static bool _freezing; + static double _time; + static bool _day; + static readonly Action StartSandstorm = (Action)Delegate.CreateDelegate(typeof(Action), typeof(Sandstorm).GetMethod("StartSandstorm", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)!); + static readonly Action StopSandstorm = (Action)Delegate.CreateDelegate(typeof(Action), typeof(Sandstorm).GetMethod("StopSandstorm", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)!); + + // Main.DoUpdate postfix. + public static void Update() + { + if (Main.gameMenu) + { + _freezing = false; + return; + } + + if (Settings.FreezeTime) + { + if (!_freezing) + { + _time = Main.time; + _day = Main.dayTime; + _freezing = true; + } + + Main.time = _time; + Main.dayTime = _day; + } + else + { + _freezing = false; + } + + if (Settings.ItemVacuum) + ItemFeatures.PullAll(); + } + + public static void SetTime(double time, bool day) + { + Main.dayTime = day; + Main.time = time; + _time = time; + _day = day; + } + +#if TMODLOADER + // tModLoader's StartRain has no instant flag. + public static void StartRain() + { + Main.StartRain(); + Main.cloudAlpha = Main.maxRaining; + } +#else + public static void StartRain() => Main.StartRain(true, null, false); +#endif + + public static void StopRain() + { +#if TMODLOADER + Main.StopRain(); +#else + Main.StopRain(true); +#endif + Main.maxRaining = 0f; + Main.cloudAlpha = 0f; + } + + public static bool SandstormActive => Sandstorm.Happening; + + // Sandstorm.StartSandstorm/StopSandstorm are private on Terraria, public on tModLoader. + public static void SetSandstorm(bool start) => (start ? StartSandstorm : StopSandstorm)(); + + public static void SetSlimeRain(bool start) + { + if (start) + Main.StartSlimeRain(true); + else + Main.StopSlimeRain(true); + } + +#if TMODLOADER + public static void DropMeteor() => WorldGen.dropMeteor(); +#else + public static void DropMeteor() => WorldGen.dropMeteor(false); +#endif + + public static void StartInvasion(int id) => Main.StartInvasion(id); + + public static void StopInvasion() + { + Main.invasionType = 0; + Main.invasionSize = 0; + } + + public static string? InvasionName + { + get + { + if (Main.invasionType == 0) + return null; + + foreach ((string name, int id) in Invasions) + if (id == Main.invasionType) + return name; + return "Invasion"; + } + } +} diff --git a/src/TerraSharp/Patches/Guard.cs b/src/TerraSharp/Patches/Guard.cs new file mode 100644 index 0000000..ea2e97b --- /dev/null +++ b/src/TerraSharp/Patches/Guard.cs @@ -0,0 +1,31 @@ +using System; +using TerraSharp.Core; + +namespace TerraSharp.Patches; + +internal static class Guard +{ + public static void Run(string where, Action action) + { + try + { + action(); + } + catch (Exception e) + { + Log.Error(where, e); + } + } + + public static void Run(string where, Action action, T argument) + { + try + { + action(argument); + } + catch (Exception e) + { + Log.Error(where, e); + } + } +} diff --git a/src/TerraSharp/Patches/HookRegistry.cs b/src/TerraSharp/Patches/HookRegistry.cs new file mode 100644 index 0000000..18b4a57 --- /dev/null +++ b/src/TerraSharp/Patches/HookRegistry.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using MonoMod.Cil; +using MonoMod.RuntimeDetour; +using TerraSharp.Core; + +namespace TerraSharp.Patches; + +internal sealed class HookRegistry : IDisposable +{ + const BindingFlags Methods = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; + static readonly DetourConfig Config = new(Plugin.Id); + readonly List _hooks = []; + + public static HookRegistry Install() + { + var hooks = new HookRegistry(); + try + { + MainPatches.Install(hooks); + PlayerPatches.Install(hooks); + NpcPatches.Install(hooks); + ItemCheckShootPatch.Install(hooks); + SandboxGuards.Install(hooks); + return hooks; + } + catch + { + hooks.Dispose(); + throw; + } + } + + public static MethodInfo Method(Type type, string name, Type[]? parameters = null) => (parameters is null ? type.GetMethod(name, Methods) : type.GetMethod(name, Methods, null, parameters, null)) ?? throw new MissingMethodException(type.FullName, name); + + public void Add(Type type, string method, Delegate callback) => Add(Method(type, method), callback); + + public void Add(MethodInfo method, Delegate callback) => _hooks.Add(new Hook(method, callback, Config)); + + public void Modify(MethodInfo method, ILContext.Manipulator callback) => _hooks.Add(new ILHook(method, callback, Config)); + + public void Dispose() + { + Exception? failure = null; + for (int i = _hooks.Count - 1; i >= 0; i--) + { + try + { + _hooks[i].Dispose(); + _hooks.RemoveAt(i); + } + catch (Exception e) + { + failure ??= e; + Log.Error("Hook disposal failed", e); + } + } + + if (failure is not null) + throw new InvalidOperationException("One or more hooks could not be removed.", failure); + } +} diff --git a/src/TerraSharp/Patches/MainPatches.cs b/src/TerraSharp/Patches/MainPatches.cs new file mode 100644 index 0000000..5f44888 --- /dev/null +++ b/src/TerraSharp/Patches/MainPatches.cs @@ -0,0 +1,162 @@ +using System; +using Microsoft.Xna.Framework; +using Terraria; +using Terraria.UI; +using TerraSharp.Core; +using TerraSharp.Features; +using TerraSharp.Simulation; +using TerraSharp.UI; + +namespace TerraSharp.Patches; + +internal static class MainPatches +{ + internal static void Install(HookRegistry hooks) + { + MainUpdatePatch.Install(hooks); + MainSetupLayersPatch.Install(hooks); + MainDrawInterfacePatch.Install(hooks); + MainDrawMenuPatch.Install(hooks); + MainDrawCursorPatch.Install(hooks); + UserInterfaceUpdatePatch.Install(hooks); + } +} + +internal static class MainUpdatePatch +{ + delegate void Original(Main self, ref GameTime gameTime); + delegate void UpdateHook(Original original, Main self, ref GameTime gameTime); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Main), "DoUpdate", (UpdateHook)Update); + + static void Update(Original original, Main self, ref GameTime gameTime) + { + original(self, ref gameTime); + Guard.Run("Main.DoUpdate postfix", Tick); + } + + static void Tick() + { + Plugin.Tick(); + if (!Plugin.Active) + return; + + Hotkeys.Update(); + Sandbox.NewTick(); + WorldFeatures.Update(); + Settings.AutoSave(); + Profiler.EndTick(); + } +} + +// The layer list is rebuilt on first draw and on resolution/UI changes. +internal static class MainSetupLayersPatch +{ + delegate void Original(Main self); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Main), "SetupDrawInterfaceLayers", (Action)SetupLayers); + + static void SetupLayers(Original original, Main self) + { + original(self); + if (Plugin.Active) + Guard.Run("SetupDrawInterfaceLayers postfix", InterfaceLayers.Install); + } +} + +// Restore the mouse even if the windows layer did not run. +internal static class MainDrawInterfacePatch +{ + delegate void Original(Main self, GameTime gameTime); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Main), "DrawInterface", (Action)DrawInterface); + + static void DrawInterface(Original original, Main self, GameTime gameTime) + { + try + { + original(self, gameTime); + } + finally + { + Guard.Run("Main.DrawInterface cleanup", InterfaceLayers.Unblock); + } + } +} + +internal static class MainDrawMenuPatch +{ + delegate void Original(Main self, GameTime gameTime); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Main), "DrawMenu", (Action)DrawMenu); + + static void DrawMenu(Original original, Main self, GameTime gameTime) + { + Guard.Run("Main.DrawMenu prefix", BeforeDraw); + + try + { + original(self, gameTime); + } + finally + { + Guard.Run("Main.DrawMenu cleanup", AfterDraw); + } + } + + static void BeforeDraw() + { + InterfaceLayers.InMenuDraw = true; + InterfaceLayers.HideMouse(); + } + + static void AfterDraw() + { + InterfaceLayers.InMenuDraw = false; + InterfaceLayers.ShowMouse(); + } +} + +internal static class MainDrawCursorPatch +{ + delegate void Original(Vector2 bonus, bool smart); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Main), nameof(Main.DrawCursor), (Action)DrawCursor); + + static void DrawCursor(Original original, Vector2 bonus, bool smart) + { + Guard.Run("Main.DrawCursor prefix", BeforeDraw); + original(bonus, smart); + } + + static void BeforeDraw() + { + if (!InterfaceLayers.InMenuDraw || !Main.gameMenu || !Plugin.Active) + return; + + InterfaceLayers.InMenuDraw = false; + InterfaceLayers.DrawMenuWindows(); + } +} + +// UIState menus read the mouse in UserInterface.Update. +internal static class UserInterfaceUpdatePatch +{ + delegate void Original(UserInterface self, GameTime gameTime); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(UserInterface), nameof(UserInterface.Update), (Action)Update); + + static void Update(Original original, UserInterface self, GameTime gameTime) + { + Guard.Run("UserInterface.Update prefix", InterfaceLayers.HideMouse); + + try + { + original(self, gameTime); + } + finally + { + Guard.Run("UserInterface.Update cleanup", InterfaceLayers.ShowMouse); + } + } +} diff --git a/src/TerraSharp/Patches/NoSpreadPatch.cs b/src/TerraSharp/Patches/NoSpreadPatch.cs new file mode 100644 index 0000000..5017bec --- /dev/null +++ b/src/TerraSharp/Patches/NoSpreadPatch.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Xna.Framework; +using Mono.Cecil.Cil; +using MonoMod.Cil; +using Terraria; +using Terraria.Utilities; +using TerraSharp.Core; + +namespace TerraSharp.Patches; + +public static class NoSpread +{ + public static bool Active { get; set; } + + public static int Next(UnifiedRandom random, int minimum, int maximum) => Active && minimum < 0 && minimum == 1 - maximum ? 0 : random.Next(minimum, maximum); + + public static float NextFloat(UnifiedRandom random) => Active ? 0.5f : random.NextFloat(); + + public static float NextFloatDirection(UnifiedRandom random) => Active ? 0f : random.NextFloatDirection(); + + public static Vector2 RotatedByRandom(Vector2 vector, double maximum) => Active ? vector : vector.RotatedByRandom(maximum); +} + +// Patch ItemCheck_Shoot call sites because the random helpers may be inlined. +internal static class ItemCheckShootPatch +{ +#if TMODLOADER + internal static void Install(HookRegistry hooks) + { + hooks.Modify(HookRegistry.Method(typeof(Player), "ItemCheck_Shoot", [typeof(int), typeof(Item), typeof(int)]), Modify); + hooks.Add(typeof(Player), "ItemCheck_Shoot", (Action, Player, int, Item, int>)Shoot); + } + + static void Shoot(Action original, Player player, int playerIndex, Item item, int damage) => WithNoSpread(player, () => original(player, playerIndex, item, damage)); +#else + internal static void Install(HookRegistry hooks) + { + hooks.Modify(HookRegistry.Method(typeof(Player), "ItemCheck_Shoot", [typeof(int), typeof(Item), typeof(int), typeof(bool)]), Modify); + hooks.Add(typeof(Player), "ItemCheck_Shoot", (Action, Player, int, Item, int, bool>)Shoot); + } + + static void Shoot(Action original, Player player, int playerIndex, Item item, int damage, bool withAudioVisualFeedback) => WithNoSpread(player, () => original(player, playerIndex, item, damage, withAudioVisualFeedback)); +#endif + + static void WithNoSpread(Player player, Action shoot) + { + bool wasActive = NoSpread.Active; + NoSpread.Active = Plugin.Active && Settings.NoSpread && player.whoAmI == Main.myPlayer && !Main.gameMenu; + + try + { + shoot(); + } + finally + { + NoSpread.Active = wasActive; + } + } + + static void Modify(ILContext context) + { + var wrappers = new Dictionary + { + [HookRegistry.Method(typeof(UnifiedRandom), nameof(UnifiedRandom.Next), [typeof(int), typeof(int)])] = HookRegistry.Method(typeof(NoSpread), nameof(NoSpread.Next)), + [HookRegistry.Method(typeof(Utils), nameof(Utils.NextFloat), [typeof(UnifiedRandom)])] = HookRegistry.Method(typeof(NoSpread), nameof(NoSpread.NextFloat)), + [HookRegistry.Method(typeof(Utils), nameof(Utils.NextFloatDirection), [typeof(UnifiedRandom)])] = HookRegistry.Method(typeof(NoSpread), nameof(NoSpread.NextFloatDirection)), + [HookRegistry.Method(typeof(Utils), nameof(Utils.RotatedByRandom), [typeof(Vector2), typeof(double)])] = HookRegistry.Method(typeof(NoSpread), nameof(NoSpread.RotatedByRandom)), + }; + + int swapped = 0; + foreach (Instruction instruction in context.Body.Instructions) + foreach (KeyValuePair wrapper in wrappers) + { + if (!instruction.MatchCallOrCallvirt(wrapper.Key)) + continue; + + instruction.OpCode = OpCodes.Call; + instruction.Operand = context.Import(wrapper.Value); + swapped++; + break; + } + + if (swapped == 0) + throw new InvalidOperationException("No Spread: ItemCheck_Shoot contains no matching random calls."); + + Log.Debug($"No Spread: rewired {swapped} random calls in ItemCheck_Shoot"); + } +} diff --git a/src/TerraSharp/Patches/NpcPatches.cs b/src/TerraSharp/Patches/NpcPatches.cs new file mode 100644 index 0000000..ff6c8fb --- /dev/null +++ b/src/TerraSharp/Patches/NpcPatches.cs @@ -0,0 +1,21 @@ +using System; +using Terraria; +using TerraSharp.Core; +using TerraSharp.Features.Combat; + +namespace TerraSharp.Patches; + +// After the AI chose its velocity, before UpdateNPC applies it. +internal static class NpcPatches +{ + delegate void Original(NPC self); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(NPC), nameof(NPC.AI), (Action)AI); + + static void AI(Original original, NPC self) + { + original(self); + if (Plugin.Active) + Guard.Run("NPC.AI postfix", CombatFeatures.PostNpcAI, self); + } +} diff --git a/src/TerraSharp/Patches/PlayerPatches.cs b/src/TerraSharp/Patches/PlayerPatches.cs new file mode 100644 index 0000000..4993401 --- /dev/null +++ b/src/TerraSharp/Patches/PlayerPatches.cs @@ -0,0 +1,101 @@ +using System; +using Terraria; +using TerraSharp.Core; +using TerraSharp.Features; +using TerraSharp.Features.Combat; +using TerraSharp.Simulation; +using TerraSharp.UI; + +namespace TerraSharp.Patches; + +internal static class PlayerPatches +{ + internal static void Install(HookRegistry hooks) + { + PlayerUpdatePatch.Install(hooks); + PlayerItemCheckPatch.Install(hooks); + PlayerMovementPatch.Install(hooks); + } +} + +// Player.Update receives whoAmI; ResetEffects and ItemCheck run after the prefix. +internal static class PlayerUpdatePatch +{ + delegate void Original(Player self, int whoAmI); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Player), nameof(Player.Update), (Action)Update); + + static bool IsLocal(int whoAmI) => Plugin.Active && whoAmI == Main.myPlayer && !Main.gameMenu; + + static void Update(Original original, Player self, int whoAmI) + { + if (IsLocal(whoAmI)) + Guard.Run("Player.Update prefix", PreUpdate, self); + + original(self, whoAmI); + + if (IsLocal(whoAmI)) + Guard.Run("Player.Update postfix", PostUpdate, self); + } + + static void PreUpdate(Player player) + { + WindowManager.ConsumeInput(player); + PlayerFeatures.PreUpdate(player); + } + + static void PostUpdate(Player player) + { + Clock.Advance(); + PlayerFeatures.PostUpdate(player); + CombatFeatures.PostPlayerUpdate(player); + Ballistics.Observe(); + NpcMotion.Track(); + } +} + +// ItemCheckWrapped runs after control input and before ItemCheck. +internal static class PlayerItemCheckPatch +{ + delegate void Original(Player self, int whoAmI); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Player), "ItemCheckWrapped", (Action)ItemCheck); + + static void ItemCheck(Original original, Player self, int whoAmI) + { + if (Plugin.Active && self.whoAmI == Main.myPlayer && !Main.gameMenu) + Guard.Run("ItemCheckWrapped prefix", BeforeItemCheck, self); + + try + { + original(self, whoAmI); + } + finally + { + Guard.Run("ItemCheckWrapped cleanup", AimOverride.Restore); + } + } + + static void BeforeItemCheck(Player player) + { + AutoFishing.PreItemCheck(player); + Aimbot.PreItemCheck(player); + ShotPreview.Update(player); + } +} + +// HorizontalMovement runs after equipment effects and before movement physics. +internal static class PlayerMovementPatch +{ + delegate void Original(Player self); + + internal static void Install(HookRegistry hooks) => hooks.Add(typeof(Player), nameof(Player.HorizontalMovement), (Action)Move); + + static void Move(Original original, Player self) + { + if (Plugin.Active && self.whoAmI == Main.myPlayer) + Guard.Run("HorizontalMovement prefix", PlayerFeatures.PreMovement, self); + + original(self); + } +} diff --git a/src/TerraSharp/Patches/SandboxGuards.cs b/src/TerraSharp/Patches/SandboxGuards.cs new file mode 100644 index 0000000..a5d15a1 --- /dev/null +++ b/src/TerraSharp/Patches/SandboxGuards.cs @@ -0,0 +1,120 @@ +using System; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using MonoMod.Cil; +using Terraria; +using TerraSharp.Simulation; + +namespace TerraSharp.Patches; + +internal static class SandboxGuards +{ + delegate void KillOriginal(Projectile projectile); + delegate void AmmoOriginal(Player player, Item item, ref int type, ref float speed, ref bool canShoot, ref int damage, ref float knockback, ref int usedAmmo, bool dontConsume); + delegate void AmmoHook(AmmoOriginal original, Player player, Item item, ref int type, ref float speed, ref bool canShoot, ref int damage, ref float knockback, ref int usedAmmo, bool dontConsume); + + public static void Install(HookRegistry hooks) + { + hooks.Add(typeof(Projectile), nameof(Projectile.Kill), (Action)Kill); + // tModLoader adds a public out-only PickAmmo that forwards to this one. + hooks.Add(HookRegistry.Method(typeof(Player), nameof(Player.PickAmmo), [typeof(Item), typeof(int).MakeByRefType(), typeof(float).MakeByRefType(), typeof(bool).MakeByRefType(), typeof(int).MakeByRefType(), typeof(float).MakeByRefType(), typeof(int).MakeByRefType(), typeof(bool)]), (AmmoHook)PickAmmo); + (Type Type, string Method)[] targets = + [ + (typeof(Projectile), nameof(Projectile.Damage)), + (typeof(Collision), nameof(Collision.SwitchTiles)), + (typeof(Terraria.Graphics.Light.LightingEngine), "AddLight"), + (typeof(Terraria.Graphics.Light.LegacyLighting), "AddLight"), + (typeof(Terraria.Audio.LegacySoundPlayer), "PlaySound"), + (typeof(Terraria.Audio.SoundPlayer), "Play"), +#if !TMODLOADER + (typeof(Terraria.Audio.SoundPlayer), "PlayLooped"), +#endif + (typeof(WorldGen), "KillTile"), + (typeof(WorldGen), "PlaceTile"), + (typeof(WorldGen), "Convert"), + (typeof(WorldGen), "KillWall"), + (typeof(WorldGen), "OpenDoor"), + (typeof(WorldGen), "CloseDoor"), + (typeof(WorldGen), "ShiftTallGate"), + (typeof(WorldGen), "ShiftTrapdoor"), + (typeof(NPC), nameof(NPC.StrikeNPC)), +#if !TMODLOADER + (typeof(NPC), nameof(NPC.StrikeNPCNoInteraction)), +#endif + (typeof(Player), nameof(Player.CollideWithNPCs)), + (typeof(Player), "TryUsingDiggerCart"), + (typeof(Player), "MowTheLawn"), + (typeof(NPC), nameof(NPC.checkDead)), + (typeof(NPC), nameof(NPC.NPCLoot)), + (typeof(NPC), nameof(NPC.Transform)), + (typeof(NetMessage), "SendData"), + (typeof(NetMessage), "TrySendData"), + ]; + const BindingFlags AllMethods = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly; + foreach ((Type type, string name) in targets) + { + int count = 0; + foreach (MethodInfo method in type.GetMethods(AllMethods)) + if (method.Name == name && !method.IsAbstract && !method.ContainsGenericParameters) + { + hooks.Modify(method, SuppressEffects); + count++; + } + if (count == 0) + throw new MissingMethodException(type.FullName, name); + } + + // Index Main.maxDust and Main.maxGore is the inactive slot the game itself returns when spawning is off. + (Type Type, string Method, int Dummy)[] spawners = [(typeof(Dust), nameof(Dust.NewDust), Main.maxDust), (typeof(Gore), nameof(Gore.NewGore), Main.maxGore)]; + foreach ((Type type, string name, int dummy) in spawners) + foreach (MethodInfo method in type.GetMethods(AllMethods)) + if (method.Name == name && method.ReturnType == typeof(int)) + hooks.Modify(method, context => ReturnInSandbox(context, dummy)); + } + + static void Kill(KillOriginal original, Projectile projectile) + { + if (Sandbox.Active) + projectile.active = false; + else + original(projectile); + } + + static void PickAmmo(AmmoOriginal original, Player player, Item item, ref int type, ref float speed, ref bool canShoot, ref int damage, ref float knockback, ref int usedAmmo, bool dontConsume) => original(player, item, ref type, ref speed, ref canShoot, ref damage, ref knockback, ref usedAmmo, dontConsume || Sandbox.DryRunning); + + static bool AllowEffects() => !Sandbox.Active; + + static void ReturnInSandbox(ILContext context, int value) + { + var cursor = new ILCursor(context); + ILLabel body = cursor.DefineLabel(); + cursor.EmitDelegate>(AllowEffects); + cursor.Emit(OpCodes.Brtrue, body); + cursor.Emit(OpCodes.Ldc_I4, value); + cursor.Emit(OpCodes.Ret); + cursor.Emit(OpCodes.Nop); + body.Target = cursor.Prev; + } + + static void SuppressEffects(ILContext context) + { + var cursor = new ILCursor(context); + ILLabel body = cursor.DefineLabel(); + cursor.EmitDelegate>(AllowEffects); + cursor.Emit(OpCodes.Brtrue, body); + if (context.Method.ReturnType.MetadataType != MetadataType.Void) + { + if (context.Method.ReturnType.IsByReference) + throw new NotSupportedException("Sandbox guards do not support ref returns."); + var result = new VariableDefinition(context.Method.ReturnType); + context.Body.Variables.Add(result); + cursor.Emit(OpCodes.Ldloca, result); + cursor.Emit(OpCodes.Initobj, result.VariableType); + cursor.Emit(OpCodes.Ldloc, result); + } + cursor.Emit(OpCodes.Ret); + cursor.Emit(OpCodes.Nop); + body.Target = cursor.Prev; + } +} diff --git a/src/TerraSharp/Polyfills/IsExternalInit.cs b/src/TerraSharp/Polyfills/IsExternalInit.cs new file mode 100644 index 0000000..361c9ff --- /dev/null +++ b/src/TerraSharp/Polyfills/IsExternalInit.cs @@ -0,0 +1,6 @@ +namespace System.Runtime.CompilerServices; + +// Lets records and init-only setters compile against .NET Framework. +internal static class IsExternalInit +{ +} diff --git a/src/TerraSharp/Simulation/Ballistics.cs b/src/TerraSharp/Simulation/Ballistics.cs new file mode 100644 index 0000000..cdbf5bd --- /dev/null +++ b/src/TerraSharp/Simulation/Ballistics.cs @@ -0,0 +1,382 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.Xna.Framework; +using Terraria; +using TerraSharp.Core; +using TerraSharp.Features.Combat; + +namespace TerraSharp.Simulation; + +// Life is measured in ticks; zero means unknown. +public sealed class FlightModel +{ + public float SpeedMul = 1f, Drag = 1f, Gravity, MaxFall = 16f; + public int Delay, Life; + public bool TileCollide = true; + public int Width = 8, Height = 8; + public bool Learned, SpeedLearned, Sandboxed; + + public void Step(int age, ref Vector2 velocity) + { + if (age <= Delay) + return; + + velocity.X *= Drag; + if (Gravity > 0f) + velocity.Y = Math.Min(velocity.Y + Gravity, Math.Max(MaxFall, velocity.Y)); + } + + public bool Blocked(Vector2 from, Vector2 to, bool pointOnly = false) => + TileCollide && TileCollision.Sweep(from, to, pointOnly ? 1 : Math.Max(1, Width), pointOnly ? 1 : Math.Max(1, Height)); + + public override string ToString() => string.Format(CultureInfo.InvariantCulture, "delay {0} drag {1:0.000} gravity {2:0.000} maxFall {3:0.0} speedMul {4:0.00} life {5}", Delay, Drag, Gravity, MaxFall, SpeedMul, Life); +} + +public static class Ballistics +{ + public sealed class SavedModel + { + public int Delay { get; set; } + public float Drag { get; set; } + public float Gravity { get; set; } + public float MaxFall { get; set; } + public float SpeedMul { get; set; } + public bool SpeedLearned { get; set; } + public int Life { get; set; } + } + + sealed class Track + { + public int Type, Age; + public bool Mine, Spoiled, Fresh; + public Vector2 Last, LastVelocity; + public float LaunchSpeed; + public readonly List Velocities = []; + } + + sealed record Fit(float Drag, float Gravity, float MaxFall, float SpeedMul, int Delay, int Life); + + const int MaxFits = 7; + const int MaxSamples = 240; + + static readonly Dictionary Models = []; + static readonly Dictionary> Fits = []; + static readonly Dictionary Logged = []; + static readonly Track?[] Tracks = new Track?[Main.maxProjectiles]; + static readonly bool[] WasActive = new bool[Main.maxProjectiles]; + static bool _observedOnce; + static int _lastShootType; + + public static float LastShootSpeed { get; private set; } + + public static void RecordShot(int projectileType, float speed) + { + _lastShootType = projectileType; + LastShootSpeed = speed; + } + + public static FlightModel Get(int type) + { + if (Models.TryGetValue(type, out FlightModel? model)) + return model; + + var sample = new Projectile(); + sample.SetDefaults(type); + model = new FlightModel { SpeedMul = 1 + sample.extraUpdates, TileCollide = sample.tileCollide, Width = sample.width, Height = sample.height }; + Models[type] = model; + + if (Sandbox.Profile(type, model)) + LogModel(type, model, "sandbox"); + + return model; + } + + public static bool Current(int slot, out Vector2 velocity, out int age) + { + Track? track = Tracks[slot]; + velocity = track?.LastVelocity ?? Vector2.Zero; + age = track?.Age ?? 0; + return track is { Velocities.Count: > 0 }; + } + + public static void Observe() + { + for (int i = 0; i < Main.maxProjectiles; i++) + { + Projectile projectile = Main.projectile[i]; + bool wasActive = WasActive[i]; + WasActive[i] = projectile.active; + Track? track = Tracks[i]; + bool flying = projectile is { active: true, bobber: false, minion: false } && !Sandbox.Held(projectile); + + if (track is not null && (!flying || track.Type != projectile.type)) + { + Learn(track); + Tracks[i] = track = null; + } + + if (!flying) + continue; + + if (track is null) + { + bool mine = projectile.owner == Main.myPlayer && projectile.friendly; + // Ignore projectiles already flying when tracking starts. + Tracks[i] = new Track { Type = projectile.type, Last = projectile.position, Mine = mine, Fresh = _observedOnce && !wasActive, LaunchSpeed = mine && projectile.type == _lastShootType ? LastShootSpeed : 0f }; + continue; + } + + Vector2 velocity = projectile.position - track.Last; + track.Last = projectile.position; + track.Age++; + + // Tile hits, bounces and homing turns show up as sharp changes. + if (!track.Spoiled && track.Velocities.Count > 0 && (velocity - track.LastVelocity).Length() > Math.Max(4f, track.LastVelocity.Length() * 0.5f)) + track.Spoiled = true; + if (!track.Spoiled && track.Velocities.Count < MaxSamples) + track.Velocities.Add(velocity); + + track.LastVelocity = velocity; + } + + _observedOnce = true; + } + + static void Learn(Track track) + { + if (track.Velocities.Count < 8 || !track.Fresh || Get(track.Type).Sandboxed) + return; + + if (!Fits.TryGetValue(track.Type, out List? list)) + Fits[track.Type] = list = []; + list.Add(FitVelocities(track.Velocities, track.Age, track.Mine ? track.LaunchSpeed : 0f)); + if (list.Count > MaxFits) + list.RemoveAt(0); + + Aggregate(track.Type, list); + } + + public static bool Apply(FlightModel model, List velocities, int life, float launchSpeed) + { + if (velocities.Count < 8) + return false; + + Fit fit = FitVelocities(velocities, life, launchSpeed); + (model.Drag, model.Delay, model.Gravity, model.MaxFall, model.Life) = (fit.Drag, fit.Delay, fit.Gravity, fit.MaxFall, fit.Life); + + if (fit.SpeedMul > 0f) + { + model.SpeedMul = fit.SpeedMul; + model.SpeedLearned = true; + } + + model.Learned = true; + return true; + } + + static float Median(IEnumerable values) + { + List sorted = [.. values]; + sorted.Sort(); + return sorted.Count == 0 ? 0f : sorted[sorted.Count / 2]; + } + + static Fit FitVelocities(List velocities, int life, float launchSpeed) + { + int delay = velocities.Count; + for (int i = 1; i < velocities.Count; i++) + { + float verticalChange = velocities[i].Y - velocities[i - 1].Y; + bool xChanged = Math.Abs(velocities[i - 1].X) > 0.3f && Math.Abs(velocities[i].X / velocities[i - 1].X - 1f) > 0.004f; + if (Math.Abs(verticalChange) > 0.01f || xChanged) + { + delay = i; + break; + } + } + + var drags = new List(); + var gravities = new List(); + float terminal = 0f; + int flat = 0; + for (int i = Math.Max(1, delay); i < velocities.Count; i++) + { + if (Math.Abs(velocities[i - 1].X) > 0.3f) + drags.Add(velocities[i].X / velocities[i - 1].X); + + float verticalChange = velocities[i].Y - velocities[i - 1].Y; + // Terminal speed only from an actual plateau of falling ticks. + if (velocities[i].Y > 1f && Math.Abs(verticalChange) < 0.01f) + { + if (++flat >= 3) + terminal = Math.Max(terminal, velocities[i].Y); + } + else + { + flat = 0; + if (verticalChange > 0.005f) + gravities.Add(verticalChange); + } + } + + float speedMul = launchSpeed > 0f ? velocities[0].Length() / launchSpeed : 0f; + if (speedMul is < 0.2f or > 20f) + speedMul = 0f; + + float maxSeen = velocities.Max(x => x.Y); + return new Fit( + Drag: drags.Count > 0 ? MathHelper.Clamp(Median(drags), 0.85f, 1.1f) : 1f, + Gravity: gravities.Count >= 3 ? Median(gravities) : 0f, + // Most vanilla projectiles cap their fall at 16 px/update. + MaxFall: terminal > 0f ? terminal : Math.Max(16f, maxSeen), + SpeedMul: speedMul, + Delay: delay >= velocities.Count ? 0 : delay, + Life: life); + } + + // Shorter flights may have ended on collision. + static void Aggregate(int type, List list) + { + FlightModel model = Get(type); + model.Drag = Median(list.Select(x => x.Drag)); + model.Delay = (int)Median(list.Select(x => (float)x.Delay)); + model.Gravity = Median(list.Select(x => x.Gravity)); + model.MaxFall = Median(list.Select(x => x.MaxFall)); + model.Life = list.Max(x => x.Life); + model.Learned = true; + + List speeds = [.. list.Where(x => x.SpeedMul > 0f).Select(x => x.SpeedMul)]; + if (speeds.Count > 0) + { + model.SpeedMul = Median(speeds); + model.SpeedLearned = true; + } + + LogModel(type, model, $"{list.Count} fits"); + } + + static void LogModel(int type, FlightModel model, string source) + { + string description = model.ToString(); + if (Settings.LogBallistics && (!Logged.TryGetValue(type, out string? last) || last != description)) + Log.Debug($"ballistics {Lang.GetProjectileName(type).Value} ({type}): {description} [{source}]"); + Logged[type] = description; + } + + public static Dictionary Save() + { + var models = new Dictionary(); + foreach (KeyValuePair entry in Models) + { + FlightModel model = entry.Value; + if (model.Learned && !model.Sandboxed) + models[entry.Key] = new SavedModel { Delay = model.Delay, Drag = model.Drag, Gravity = model.Gravity, MaxFall = model.MaxFall, SpeedMul = model.SpeedMul, SpeedLearned = model.SpeedLearned, Life = model.Life }; + } + + return models; + } + + public static void Load(int type, SavedModel model) + { + FlightModel current = Get(type); + if (current.Sandboxed) + return; + + current.Delay = model.Delay; + current.Drag = model.Drag; + current.Gravity = model.Gravity; + current.MaxFall = model.MaxFall; + current.SpeedMul = model.SpeedMul; + current.SpeedLearned = model.SpeedLearned; + current.Life = model.Life; + current.Learned = true; + + Fits[type] = [new Fit(current.Drag, current.Gravity, current.MaxFall, current.SpeedLearned ? current.SpeedMul : 0f, current.Delay, current.Life)]; + Logged[type] = current.ToString(); + } + + public static void Reset() + { + Models.Clear(); + Fits.Clear(); + Logged.Clear(); + Log.Info("ballistics reset"); + } + + public static int Path(FlightModel model, Vector2 position, Vector2 velocity, int age, int ticks, Vector2[] output) + { + int count = 0; + output[count++] = position; + + for (int t = 1; t <= ticks && count < output.Length; t++) + { + model.Step(age + t, ref velocity); + Vector2 next = position + velocity; + output[count++] = next; + if (model.Blocked(position, next)) + break; + + position = next; + } + + return count; + } + + public static bool Solve(int type, float speed, Vector2 origin, Vector2 target, NPC? mover, float tolerance, out Vector2 direction) + { + FlightModel model = Get(type); + float launchVelocity = Math.Max(0.1f, speed * model.SpeedMul); + int maxTicks = model.Life > 0 ? Math.Min(240, model.Life) : 240; + Vector2 Goal(int t) => mover is null ? target : target + NpcMotion.Offset(mover, t); + + Vector2 aim = target; + direction = Vector2.Normalize(target - origin); + float bestMiss = float.MaxValue; + bool blocked = false; + + for (int iteration = 0; iteration < 8; iteration++) + { + Vector2 position = origin, velocity = direction * launchVelocity, best = position; + float bestDistance = float.MaxValue; + int bestTick = 0; + blocked = false; + + for (int t = 1; t <= maxTicks; t++) + { + model.Step(t, ref velocity); + Vector2 next = position + velocity; + float distance = Vector2.DistanceSquared(next, Goal(t)); + if (distance < bestDistance) + { + bestDistance = distance; + best = next; + bestTick = t; + } + + // The game lets a projectile spawn overlapping ground next to the player. + if (model.Blocked(position, next, t <= 2)) + { + // Landing behind an enemy the shot already passed through is fine. + blocked = bestDistance > tolerance * tolerance; + break; + } + + position = next; + if (t > bestTick + 30) + break; + } + + Vector2 miss = Goal(bestTick) - best; + bestMiss = miss.Length(); + if (bestMiss < 2f) + break; + + aim += miss; + direction = Vector2.Normalize(aim - origin); + } + + return !blocked && bestMiss <= tolerance; + } +} diff --git a/src/TerraSharp/Simulation/Sandbox.cs b/src/TerraSharp/Simulation/Sandbox.cs new file mode 100644 index 0000000..855c2f9 --- /dev/null +++ b/src/TerraSharp/Simulation/Sandbox.cs @@ -0,0 +1,640 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using Microsoft.Xna.Framework; +using Terraria; +using Terraria.Utilities; +#if TMODLOADER +using Terraria.ModLoader; +#endif +using TerraSharp.Core; + +namespace TerraSharp.Simulation; + +// Side effects are suppressed by Patches/SandboxGuards; dust and gore go to the game's dummy slot. +public static class Sandbox +{ + // Animation is the owner's item animation length for stabs, 0 otherwise. + public sealed record Shot(Projectile Projectile, int Slot, int Animation); + + public static bool Active { get; private set; } + + public static bool DryRunning { get; private set; } + + const BindingFlags AnyInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + static readonly Func Clone = (Func)Delegate.CreateDelegate(typeof(Func), typeof(object).GetMethod("MemberwiseClone", AnyInstance)!); + static readonly HashSet HeldStyles = [7, 13, 15, 20, 26, 62, 66, 67, 75, 99, 140, 142, 165, 190, 191]; + static readonly HashSet FailedTypes = []; + static readonly HashSet FailedWeapons = []; + static readonly UnifiedRandom Random = new(12345); +#if TMODLOADER + static readonly Action Shoot = (Action)Delegate.CreateDelegate(typeof(Action), typeof(Player).GetMethod("ItemCheck_Shoot", AnyInstance)!); +#else + static readonly Action Shoot = (Action)Delegate.CreateDelegate(typeof(Action), typeof(Player).GetMethod("ItemCheck_Shoot", AnyInstance)!); +#endif + static readonly Action ApplyItemAnimation = (Action)Delegate.CreateDelegate(typeof(Action), typeof(Player).GetMethod("ApplyItemAnimation", AnyInstance, null, [typeof(Item)], null)!); + static readonly Action CopyPlayer = BuildFieldCopier(); + + delegate void MeleeHitbox(Player player, Item item, Rectangle heldItemFrame, out bool dontAttack, out Rectangle itemRectangle); + + static readonly MeleeHitbox GetMeleeHitbox = (MeleeHitbox)Delegate.CreateDelegate(typeof(MeleeHitbox), typeof(Player).GetMethod("ItemCheck_GetMeleeHitbox", AnyInstance)!); + + static readonly EntityCopier ProjectileCopier = new(); + static readonly EntityCopier NpcCopier = new(); +#if TMODLOADER + static readonly Action UpdateGravity = (Action)Delegate.CreateDelegate(typeof(Action), typeof(NPC).GetMethod("UpdateNPC_UpdateGravity", AnyInstance)!); +#else + delegate void GravityStep(NPC npc, out float maxFallSpeed); + + static readonly GravityStep UpdateGravity = (GravityStep)Delegate.CreateDelegate(typeof(GravityStep), typeof(NPC).GetMethod("UpdateNPC_UpdateGravity", AnyInstance)!); + static readonly FieldInfo Gravity = typeof(NPC).GetField("gravity", BindingFlags.Static | BindingFlags.NonPublic)!; +#endif + static readonly Action UpdateCollision = (Action)Delegate.CreateDelegate(typeof(Action), typeof(NPC).GetMethod("UpdateCollision", AnyInstance)!); +#if TMODLOADER + // NPC.AI runs GlobalNPC hooks, whose per-entity instances are shared with the live NPC. + static readonly Action RunAI = (Action)Delegate.CreateDelegate(typeof(Action), typeof(NPC).GetMethod("VanillaAI", AnyInstance, null, Type.EmptyTypes, null)!); +#else + static readonly Action RunAI = (Action)Delegate.CreateDelegate(typeof(Action), typeof(NPC).GetMethod("AI", AnyInstance, null, Type.EmptyTypes, null)!); +#endif + static readonly HashSet FailedNpcTypes = []; + + static Projectile[] _projectiles = []; + static Projectile[] _realProjectiles = []; + static UnifiedRandom? _realRandom; + static Player? _savedPlayer; + static NPC[] _npcs = []; + static NPC[] _realNpcs = []; + static Array?[][] _npcArrays = []; + static int _npcSnapshotTick = -1; + + static IEnumerable InstanceFields(Type? type) + { + for (; type is not null && type != typeof(object); type = type.BaseType) + foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) + yield return field; + } + + // Reference fields are copied without cloning. + static Action BuildFieldCopier() + { + var method = new DynamicMethod("Copy" + typeof(T).Name, null, [typeof(T), typeof(T)], typeof(Sandbox).Module, true); + ILGenerator generator = method.GetILGenerator(); + + foreach (FieldInfo field in InstanceFields(typeof(T))) + { + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Ldarg_1); + generator.Emit(OpCodes.Ldfld, field); + generator.Emit(OpCodes.Stfld, field); + } + + generator.Emit(OpCodes.Ret); + return (Action)method.CreateDelegate(typeof(Action)); + } + + // Field copies whose array fields are never shared with the source. + sealed class EntityCopier where T : class + { + readonly Action _copy = BuildFieldCopier(); + readonly Func[] _get; + readonly Action[] _set; + + public int ArrayCount => _get.Length; + + public EntityCopier() + { + FieldInfo[] fields = [.. InstanceFields(typeof(T)).Where(f => f.FieldType.IsArray)]; + _get = [.. fields.Select(Getter)]; + _set = [.. fields.Select(Setter)]; + } + + static Func Getter(FieldInfo field) + { + var method = new DynamicMethod("Get" + field.Name, typeof(Array), [typeof(T)], typeof(Sandbox).Module, true); + ILGenerator generator = method.GetILGenerator(); + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Ldfld, field); + generator.Emit(OpCodes.Ret); + return (Func)method.CreateDelegate(typeof(Func)); + } + + static Action Setter(FieldInfo field) + { + var method = new DynamicMethod("Set" + field.Name, null, [typeof(T), typeof(Array)], typeof(Sandbox).Module, true); + ILGenerator generator = method.GetILGenerator(); + generator.Emit(OpCodes.Ldarg_0); + generator.Emit(OpCodes.Ldarg_1); + generator.Emit(OpCodes.Castclass, field.FieldType); + generator.Emit(OpCodes.Stfld, field); + generator.Emit(OpCodes.Ret); + return (Action)method.CreateDelegate(typeof(Action)); + } + + public T Duplicate(T source) + { + var copy = (T)Clone(source); + for (int k = 0; k < _get.Length; k++) + if (_get[k](source) is Array array) + _set[k](copy, (Array)array.Clone()); + + return copy; + } + + // Owned holds the target's arrays between calls. + public void Copy(T target, T source, Array?[] owned) + { + _copy(target, source); + for (int k = 0; k < _get.Length; k++) + { + if (_get[k](source) is not Array array) + continue; + + Array? mine = owned[k]; + if (mine is null || mine.Length != array.Length || mine.GetType() != array.GetType()) + mine = owned[k] = (Array)array.Clone(); + else + Array.Copy(array, mine, array.Length); + _set[k](target, mine); + } + } + } + + // Spear and shortsword AI derives position from the owner's item animation. + public static bool Stabs(Projectile projectile) => projectile.aiStyle is 19 or 161; + + public static bool Held(Projectile projectile) => Stabs(projectile) || HeldStyles.Contains(projectile.aiStyle) || (projectile.owner is >= 0 and < Main.maxPlayers && Main.player[projectile.owner].heldProj == projectile.whoAmI); + + // Stab simulations advance the owner's item animation. + public static bool CanFly(Projectile projectile) + { + if (projectile.minion || projectile.sentry || projectile.bobber || FailedTypes.Contains(projectile.type)) + return false; + + return Stabs(projectile) || !Held(projectile); + } + +#if TMODLOADER + public static bool CanDryRun(Item item) => item.shoot > 0 && item.damage > 0 && !item.CountsAsClass(DamageClass.Summon) && !item.sentry && !item.channel && !FailedWeapons.Contains(item.type); +#else + public static bool CanDryRun(Item item) => item.shoot > 0 && item.damage > 0 && !item.summon && !item.sentry && !item.channel && !FailedWeapons.Contains(item.type); +#endif + + static void SavePlayer(Player player) + { + _savedPlayer ??= (Player)Clone(player); + CopyPlayer(_savedPlayer, player); + } + + static void RestorePlayer(Player player) => CopyPlayer(player, _savedPlayer!); + + // Scratch projectiles receive spawns; Main.rand uses a separate generator. + static void Enter() + { + if (_projectiles.Length != Main.projectile.Length) + { + _projectiles = new Projectile[Main.projectile.Length]; + for (int i = 0; i < _projectiles.Length; i++) + _projectiles[i] = new Projectile(); + } + + (_realProjectiles, _realRandom) = (Main.projectile, Main.rand); + (Main.projectile, Main.rand) = (_projectiles, Random); + Active = true; + } + + static void Exit() + { + Active = false; + (Main.projectile, Main.rand) = (_realProjectiles, _realRandom); + + foreach (Projectile projectile in _projectiles) + if (projectile.active) + projectile.active = false; + } + + public static int Fly(Shot shot, int ticks, Vector2[] path, Func? stop = null) => Fly(shot.Projectile, shot.Slot, ticks, path, shot.Animation, stop); + + // Both stab styles count their updates in ai[0]; the game decrements itemAnimation before each projectile update. + static void Animate(Player player, Projectile stab, int animation) + { + player.itemAnimationMax = animation; + player.itemAnimation = Math.Max(0, animation - (int)stab.ai[0]); + } + + // Returns -1 when simulation fails or is already active; stop gets each new path index and ends the flight early. + public static int Fly(Projectile copy, int slot, int ticks, Vector2[] path, int animation = 0, Func? stop = null) + { + if (Active) + return -1; + + Player? owner = animation > 0 && Stabs(copy) && copy.owner is >= 0 and < Main.maxPlayers ? Main.player[copy.owner] : null; + if (owner is not null) + SavePlayer(owner); + Enter(); + Projectile parked = _projectiles[slot]; + _projectiles[slot] = copy; + + try + { + int count = 0; + path[count++] = copy.Center; + + for (int t = 0; t < ticks && count < path.Length; t++) + { + if (owner is not null) + Animate(owner, copy, animation); + copy.Update(slot); + if (!copy.active) + break; + path[count++] = copy.Center; + if (stop is not null && stop(count - 1)) + break; + } + + return count; + } + catch (Exception e) + { + FailedTypes.Add(copy.type); + Log.Error($"Sandbox fly {copy.type}", e); + return -1; + } + finally + { + _projectiles[slot] = parked; + Exit(); + if (owner is not null) + RestorePlayer(owner); + } + } + +#if TMODLOADER + // A ModNPC's Entity is the live NPC, so its AI would move the real one. + public static bool CanMove(NPC npc) => npc.active && npc.ModNPC is null && !FailedNpcTypes.Contains(npc.type); +#else + public static bool CanMove(NPC npc) => npc.active && !FailedNpcTypes.Contains(npc.type); +#endif + + // Scratch NPCs get their own copies of the array fields so AI writes stay out of the live NPCs. + static void CopyNpc(int slot) + { + NPC source = _realNpcs[slot], scratch = _npcs[slot]; + if (source.active) + { + NpcCopier.Copy(scratch, source, _npcArrays[slot]); + return; + } + + scratch.active = false; + scratch.whoAmI = slot; + } + + // All NPCs are copied once per tick; the simulated slot is refreshed before and after each run. + static void EnterNpcs(int slot) + { + _realNpcs = Main.npc; + if (_npcs.Length != _realNpcs.Length) + { + _npcs = new NPC[_realNpcs.Length]; + _npcArrays = new Array?[_realNpcs.Length][]; + for (int i = 0; i < _npcs.Length; i++) + (_npcs[i], _npcArrays[i]) = (new NPC(), new Array?[NpcCopier.ArrayCount]); + _npcSnapshotTick = -1; + } + + if (_npcSnapshotTick != Clock.Tick) + { + _npcSnapshotTick = Clock.Tick; + for (int i = 0; i < _npcs.Length; i++) + CopyNpc(i); + } + else + { + CopyNpc(slot); + } + + Main.npc = _npcs; + } + + // Movement part of NPC.UpdateNPC; buffs, damage, despawn and network code are left out. + static void Step(NPC npc) + { +#if TMODLOADER + UpdateGravity(npc); + RunAI(npc); + + if (!npc.noGravity) + npc.velocity.Y = Math.Min(npc.velocity.Y + npc.gravity, npc.maxFallSpeed); +#else + UpdateGravity(npc, out float maxFallSpeed); + RunAI(npc); + + if (!npc.noGravity) + npc.velocity.Y = Math.Min(npc.velocity.Y + (float)Gravity.GetValue(null), maxFallSpeed); +#endif + for (int i = 0; i < npc.immune.Length; i++) + if (npc.immune[i] > 0) + npc.immune[i]--; + if (Math.Abs(npc.velocity.X) < 0.005f) + npc.velocity.X = 0f; + + if (npc.noTileCollide) + { + npc.oldPosition = npc.position; + npc.position += npc.velocity; + } + else + { + UpdateCollision(npc); + } + + npc.justHit = false; + } + + // Records npc.position and npc.velocity per tick; afterTick runs where the game updates projectiles, with the tick number. + public static int Move(NPC npc, int ticks, Vector2[] path, Vector2[] velocities, Action? afterTick = null) + { + if (Active || !CanMove(npc)) + return -1; + + int slot = npc.whoAmI; +#if !TMODLOADER + object gravity = Gravity.GetValue(null); +#endif + EnterNpcs(slot); + Enter(); + + try + { + NPC copy = _npcs[slot]; + int count = 0; + (path[count], velocities[count]) = (copy.position, copy.velocity); + count++; + + for (int t = 0; t < ticks && count < path.Length; t++) + { + Step(copy); + if (!copy.active) + break; + (path[count], velocities[count]) = (copy.position, copy.velocity); + count++; + afterTick?.Invoke(copy, t + 1); + } + + return count; + } + catch (Exception e) + { + FailedNpcTypes.Add(npc.type); + Log.Error($"Sandbox move {npc.type}", e); + return -1; + } + finally + { + Exit(); + Main.npc = _realNpcs; + CopyNpc(slot); +#if !TMODLOADER + Gravity.SetValue(null, gravity); +#endif + } + } + + public static List? DryRun(Player player, Item item, Vector2 aimWorld) + { + if (Active || !CanDryRun(item)) + return null; + + SavePlayer(player); + (int X, int Y) mouse = GameMouse.Save(); + List? shots = []; + + Enter(); + DryRunning = true; + + try + { + GameMouse.PointAt(aimWorld); + ApplyItemAnimation(player, item); + int animation = player.itemAnimationMax; +#if TMODLOADER + Shoot(player, player.whoAmI, item, player.GetWeaponDamage(item)); +#else + Shoot(player, player.whoAmI, item, player.GetWeaponDamage(item), false); +#endif + + var seen = new bool[_projectiles.Length]; + var stabs = new List(); + for (int i = 0; i < _projectiles.Length; i++) + { + Projectile projectile = _projectiles[i]; + if (!projectile.active || !CanFly(projectile)) + continue; + + seen[i] = true; + bool stab = Stabs(projectile); + shots.Add(new Shot(ProjectileCopier.Duplicate(projectile), i, stab ? animation : 0)); + if (stab) + stabs.Add(projectile); + } + + // Stabs like the Storm Spear launch their shot from their own update. + for (int t = 0; t < Math.Min(60, Math.Max(16, animation)) && stabs.Any(x => x.active); t++) + { + foreach (Projectile stab in stabs.Where(x => x.active)) + { + Animate(player, stab, animation); + stab.Update(stab.whoAmI); + } + + for (int i = 0; i < _projectiles.Length; i++) + if (!seen[i] && _projectiles[i].active && CanFly(_projectiles[i]) && !Stabs(_projectiles[i])) + { + seen[i] = true; + shots.Add(new Shot(ProjectileCopier.Duplicate(_projectiles[i]), i, 0)); + } + } + } + catch (Exception e) + { + FailedWeapons.Add(item.type); + Log.Error($"Sandbox dry run {item.type}", e); + shots = null; + } + finally + { + DryRunning = false; + Exit(); + RestorePlayer(player); + GameMouse.Restore(mouse); + } + + return shots is { Count: > 0 } ? shots : null; + } + + // Held keys stay held; vertical movement is velocity, gravity and tiles only. + static void PredictPlayer(Player player, Vector2[] path, int length) + { + player.itemAnimation = 1; + path[0] = player.position; + for (int t = 1; t < length; t++) + { + player.HorizontalMovement(); + if (!player.controlJump && player.grappling[0] < 0 && !player.mount.Active) + player.velocity.Y = player.gravDir > 0f ? Math.Min(player.velocity.Y + player.gravity, player.maxFallSpeed) : Math.Max(player.velocity.Y - player.gravity, -player.maxFallSpeed); + player.velocity = Collision.TileCollision(player.position, player.velocity, player.width, player.height, player.controlDown, false, (int)player.gravDir); + player.position += player.velocity; + path[t] = player.position; + } + } + + // Fills playerPath, then the melee hitbox of a swing started now per tick, facing the player's way and the other way; Rectangle.Empty where the item can't hit. + public static int Swing(Player player, Item item, bool predictMovement, Vector2[] playerPath, int pathLength, Rectangle[] forward, Rectangle[]? backward) + { + if (Active || FailedWeapons.Contains(item.type)) + return -1; + + SavePlayer(player); + int direction = player.direction, count = 0; + Enter(); + + try + { + if (predictMovement) + PredictPlayer(player, playerPath, pathLength); + else + for (int t = 0; t < pathLength; t++) + playerPath[t] = player.position; + + for (int pass = 0; pass < 2; pass++) + { + Rectangle[]? boxes = pass == 0 ? forward : backward; + int facing = pass == 0 ? direction : -direction; + if (boxes is null) + continue; + + RestorePlayer(player); + player.direction = facing; + player.lastVisualizedSelectedItem = item; + ApplyItemAnimation(player, item); +#if TMODLOADER + int animation = player.itemAnimationMax; +#else + // Vanilla decrements itemAnimation in the tick the use starts. + int animation = player.itemAnimationMax - 1; +#endif + for (count = 0; count < Math.Min(animation, Math.Min(pathLength, boxes.Length)); count++) + { + player.itemAnimation = animation - count; + player.position = playerPath[count]; + Rectangle frame = Item.GetDrawHitbox(item.type, player); + player.ItemCheck_ApplyUseStyle(player.HeightOffsetHitboxCenter, item, frame); + GetMeleeHitbox(player, item, frame, out bool dontAttack, out Rectangle box); + boxes[count] = dontAttack ? Rectangle.Empty : box; + } + } + } + catch (Exception e) + { + FailedWeapons.Add(item.type); + Log.Error($"Sandbox swing {item.type}", e); + count = -1; + } + finally + { + Exit(); + RestorePlayer(player); + } + + return count; + } + + static readonly Vector2[] ProfilePoints = new Vector2[601]; + + public static bool Profile(int type, FlightModel model) + { + if (FailedTypes.Contains(type) || Main.gameMenu || Active) + return false; + + var projectile = new Projectile(); + projectile.SetDefaults(type); + if (!CanFly(projectile) || Stabs(projectile)) + return false; + + const float LaunchSpeed = 8f; + const int MaxTicks = 600; + int slot = Main.maxProjectiles - 1; + projectile.owner = Main.myPlayer; + projectile.whoAmI = slot; + projectile.tileCollide = false; + projectile.damage = 0; + projectile.Center = Main.LocalPlayer.Center; + projectile.velocity = new Vector2(LaunchSpeed, 0f); + projectile.active = true; + + int count = Fly(projectile, slot, MaxTicks, ProfilePoints); + if (count < 9) + { + if (count >= 0) + FailedTypes.Add(type); + return false; + } + + var velocities = new List(count); + for (int i = 1; i < count; i++) + velocities.Add(ProfilePoints[i] - ProfilePoints[i - 1]); + + int life = count - 1 >= MaxTicks ? 0 : count - 1; + if (!Ballistics.Apply(model, velocities, life, LaunchSpeed)) + return false; + + model.Sandboxed = true; + return true; + } + + sealed class CachedPath + { + public int Type, At, Count; + public readonly Vector2[] Points = new Vector2[181]; + public readonly Projectile Scratch = new(); + public readonly Array?[] Arrays = new Array?[ProjectileCopier.ArrayCount]; + } + + static readonly Dictionary Live = []; + static int _budget; + + public static void NewTick() => _budget = 4; + + // Cached paths may be up to 10 ticks old. + public static int LivePath(Projectile projectile, int slot, int tick, out Vector2[] path, out int from) + { + path = []; + from = 0; + if (!CanFly(projectile)) + return -1; + + Live.TryGetValue(slot, out CachedPath? cached); + bool stale = cached is null || cached.Type != projectile.type || tick - cached.At > 10 || tick < cached.At; + if (stale && _budget-- > 0) + { + cached ??= Live[slot] = new CachedPath(); + ProjectileCopier.Copy(cached.Scratch, projectile, cached.Arrays); + int count = Fly(cached.Scratch, slot, Math.Min(180, Math.Max(1, projectile.timeLeft)), cached.Points); + if (count < 0) + return -1; + + (cached.Type, cached.At, cached.Count) = (projectile.type, tick, count); + } + else if (stale && (cached is null || cached.Type != projectile.type)) + { + return -1; + } + + path = cached!.Points; + from = Math.Min(Math.Max(0, tick - cached.At), Math.Max(0, cached.Count - 1)); + return cached.Count; + } +} diff --git a/src/TerraSharp/Simulation/TileCollision.cs b/src/TerraSharp/Simulation/TileCollision.cs new file mode 100644 index 0000000..7829c5f --- /dev/null +++ b/src/TerraSharp/Simulation/TileCollision.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.Xna.Framework; +using Terraria; + +namespace TerraSharp.Simulation; + +// Collision.CanHitLine also checks tiles above and below the line. +public static class TileCollision +{ + static bool IsSolid(int x, int y, out int top) + { + top = y * 16; + if (x < 0 || y < 0 || x >= Main.maxTilesX || y >= Main.maxTilesY) + return false; + Tile tile = Main.tile[x, y]; +#if TMODLOADER + if (!tile.HasTile || tile.IsActuated || !Main.tileSolid[tile.TileType] || Main.tileSolidTop[tile.TileType]) + return false; + if (tile.IsHalfBlock) + top += 8; +#else + if (tile is null || !tile.active() || tile.inActive() || !Main.tileSolid[tile.type] || Main.tileSolidTop[tile.type]) + return false; + if (tile.halfBrick()) + top += 8; +#endif + return true; + } + + public static bool BoxSolid(Vector2 center, int width, int height) + { + float left = center.X - width / 2f, right = center.X + width / 2f, top = center.Y - height / 2f, bottom = center.Y + height / 2f; + for (int x = (int)(left / 16f); x <= (int)((right - 0.01f) / 16f); x++) + for (int y = (int)(top / 16f); y <= (int)((bottom - 0.01f) / 16f); y++) + if (IsSolid(x, y, out int tileTop) && bottom > tileTop) + return true; + return false; + } + + // Sweeps the box from 'from' to 'to' in steps of at most 4 px. + public static bool Sweep(Vector2 from, Vector2 to, int width, int height) + { + int steps = Math.Max(1, (int)Math.Ceiling(Vector2.Distance(from, to) / 4f)); + for (int i = 1; i <= steps; i++) + if (BoxSolid(Vector2.Lerp(from, to, i / (float)steps), width, height)) + return true; + return false; + } +} diff --git a/src/TerraSharp/StartupHook.cs b/src/TerraSharp/StartupHook.cs new file mode 100644 index 0000000..125074d --- /dev/null +++ b/src/TerraSharp/StartupHook.cs @@ -0,0 +1,13 @@ +using System; + +// DOTNET_STARTUP_HOOKS runs before tModLoader's entry point. +public static class StartupHook +{ + public static void Initialize() + { + // tModLoader relaunches itself with -terrariasteamclient, and child processes inherit the environment. + if (Array.Exists(Environment.GetCommandLineArgs(), a => a is "-server" or "-terrariasteamclient" or "-tModPorter" or "-build")) + return; + TerraSharp.Autoload.Start(); + } +} diff --git a/src/TerraSharp/TerraSharp.csproj b/src/TerraSharp/TerraSharp.csproj new file mode 100644 index 0000000..521efe5 --- /dev/null +++ b/src/TerraSharp/TerraSharp.csproj @@ -0,0 +1,50 @@ + + + + Library + TerraSharp + + + + $(DefineConstants);TERRASHARP_LOGGING + + + + TerraSharp.Vanilla + true + + + + TerraSharp.tModLoader + $(DefineConstants);TMODLOADER + false + false + + + + + + + + + <_EmbeddedDependency Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.NuGetPackageId)' != '' and '%(ReferenceCopyLocalPaths.Extension)' == '.dll'" /> + + + + + + + + + + + + + + + + + + + + diff --git a/src/TerraSharp/UI/BuffBrowser.cs b/src/TerraSharp/UI/BuffBrowser.cs new file mode 100644 index 0000000..73c6e95 --- /dev/null +++ b/src/TerraSharp/UI/BuffBrowser.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Terraria; +using Terraria.ID; +#if TMODLOADER +using Terraria.ModLoader; +#endif +using TerraSharp.Core; +using TerraSharp.Features; + +namespace TerraSharp.UI; + +public sealed class BuffBrowser() : GridBrowser("buffs", "Buff Browser", new Vector2(660, 160), 480, 540, "Search buffs by name or ID", "buffs", new IdIndex(() => BuffCount, Lang.GetBuffName), Categories) +{ + const string DurationSlider = "buffDuration"; + +#if TMODLOADER + static int BuffCount => BuffLoader.BuffCount; +#else + static int BuffCount => BuffID.Count; +#endif + +#if TMODLOADER + static HashSet? _mountBuffs; +#endif + + static readonly BrowserCategory[] Categories = [new("All", _ => true), new("Buffs", type => !Main.debuff[type] && !IsPet(type) && !IsMount(type)), new("Debuffs", type => Main.debuff[type]), new("Pets", IsPet), new("Mounts", IsMount)]; + + static bool IsPet(int type) => Main.vanityPet[type] || Main.lightPet[type]; + +#if TMODLOADER + static bool IsMount(int type) + { + if (_mountBuffs is null) + { + _mountBuffs = []; + foreach (Mount.MountData? mount in Mount.mounts) + if (mount is not null) + _mountBuffs.UnionWith([mount.buff, mount.extraBuff]); + } + return _mountBuffs.Contains(type); + } +#else + static bool IsMount(int type) => BuffID.Sets.MountType[type] >= 0; +#endif + + static string FormatTime(int ticks) + { + int seconds = ticks / 60; + return seconds >= 3600 ? $"{seconds / 3600}h" : seconds >= 60 ? $"{seconds / 60}m" : $"{seconds}s"; + } + + protected override void DrawContent(Rectangle body) + { + int activeRow = body.Y + 28; + Widgets.Text("Active", new Vector2(body.X + 2, body.Y + 2), Widgets.Header, 0.85f); + DrawActive(body, activeRow); + DrawSearchGrid(body, activeRow + 44, body.Bottom - 40); + + var duration = new Rectangle(body.X + 2, body.Bottom - 34, body.Width - 4, 30); + bool hot = Widgets.Hot(duration) || Widgets.ActiveSlider == DurationSlider; + Render.TextMid(Widgets.SpriteBatch, $"Duration: {(int)Settings.BuffMinutes} min", duration, hot ? Widgets.Yellow : Color.White, 0.85f); + Settings.BuffMinutes = (float)Math.Round(Widgets.ValueBar(DurationSlider, duration, Settings.BuffMinutes, 1f, 60f, 10f)); + } + + void DrawActive(Rectangle body, int y) + { + Player player = Main.LocalPlayer; + int x = body.X; + bool any = false; + for (int i = 0; i < player.buffType.Length; i++) + { + int type = player.buffType[i]; + if (type <= 0 || player.buffTime[i] <= 0) + continue; + + any = true; + if (x + 36 > body.Right) + break; + + var bounds = new Rectangle(x, y, 36, 36); + bool hot = Widgets.Hot(bounds); + Widgets.Slot(bounds, hot); + Widgets.Icon(GameTextures.Get("Buff", type), new Rectangle(bounds.X + 4, bounds.Y + 4, 28, 28)); + if (!Main.buffNoTimeDisplay[type]) + Render.TextCentered(Widgets.SpriteBatch, FormatTime(player.buffTime[i]), new Rectangle(bounds.X, bounds.Bottom - 12, bounds.Width, 14), Color.White, 0.6f); + + if (hot) + { + Widgets.Tooltip = $"{Index.Name(type)}\n{Lang.GetBuffDescription(type)}\nClick to remove"; + if (Widgets.Click) + { + player.DelBuff(i); + Widgets.Sound(12); + break; + } + } + + x += 40; + } + + if (!any) + Render.TextMid(Widgets.SpriteBatch, "No active buffs", new Rectangle(body.X + 2, y, body.Width, 36), Widgets.Dim * 0.8f, 0.8f); + } + + protected override void DrawSlot(int type, Rectangle bounds, bool hot) + { + Widgets.Slot(bounds, hot, Array.IndexOf(Main.LocalPlayer.buffType, type) >= 0); + Widgets.Icon(GameTextures.Get("Buff", type), new Rectangle(bounds.X + 6, bounds.Y + 6, bounds.Width - 12, bounds.Height - 12)); + if (!hot) + return; + + string kind = Main.debuff[type] ? "Debuff" : "Buff"; + Widgets.Tooltip = $"{Index.Name(type)}\n{Lang.GetBuffDescription(type)}\n{kind} #{type} Click to apply"; + if (!Widgets.Click) + return; + + PlayerFeatures.AddBuff(type); + Widgets.Sound(12); + } +} diff --git a/src/TerraSharp/UI/GameTextures.cs b/src/TerraSharp/UI/GameTextures.cs new file mode 100644 index 0000000..764548f --- /dev/null +++ b/src/TerraSharp/UI/GameTextures.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Terraria; +using Terraria.GameContent; +using TerraSharp.Core; + +namespace TerraSharp.UI; + +// Cache ReLogic Assets through reflection; resource packs can dispose their textures. +public static class GameTextures +{ + static readonly Dictionary ValueProperties = []; + static readonly Dictionary Fields = []; + static readonly Dictionary Requested = []; + static MethodInfo? _request; + static object? _repository, _immediateLoad; + + static Texture2D? Value(object? asset) + { + if (asset is null) + return null; + + Type type = asset.GetType(); + if (!ValueProperties.TryGetValue(type, out PropertyInfo? property)) + ValueProperties[type] = property = type.GetProperty("Value")!; + return property.GetValue(asset, null) is Texture2D { IsDisposed: false } texture ? texture : null; + } + + static FieldInfo? Field(string name) + { + if (!Fields.TryGetValue(name, out FieldInfo? field)) + Fields[name] = field = typeof(TextureAssets).GetField(name, BindingFlags.Public | BindingFlags.Static); + return field; + } + + // TextureAssets. + public static Texture2D? Get(string name) => Value(Field(name)?.GetValue(null)); + + // TextureAssets.[index], e.g. Buff + public static Texture2D? Get(string name, int index) => + Field(name)?.GetValue(null) is Array array && index >= 0 && index < array.Length ? Value(array.GetValue(index)) : null; + + // ReLogic AssetRepository.Request requires reflection. + public static Texture2D? Request(string path) + { + if (!Requested.TryGetValue(path, out object? asset)) + { + try + { + if (_request is null) + { + FieldInfo assets = typeof(Main).GetField("Assets", BindingFlags.Public | BindingFlags.Static)!; + _repository = assets.GetValue(null); + MethodInfo request = assets.FieldType.GetMethods().First(m => m.Name == "Request" && m.IsGenericMethodDefinition && m.GetParameters().Length == 2); + _request = request.MakeGenericMethod(typeof(Texture2D)); + _immediateLoad = Enum.Parse(request.GetParameters()[1].ParameterType, "ImmediateLoad"); + } + + asset = _request.Invoke(_repository, [path, _immediateLoad]); + } + catch (Exception e) + { + Log.Error($"Texture request {path}", e); + asset = null; + } + + Requested[path] = asset; + } + + try + { + return Value(asset); + } + catch + { + return null; + } + } + + public static Rectangle Frame(Texture2D texture, int horizontalFrames, int verticalFrames, int x, int y) + { + int width = texture.Width / horizontalFrames, height = texture.Height / verticalFrames; + return new Rectangle(width * x, height * y, width, height); + } +} diff --git a/src/TerraSharp/UI/GridBrowser.cs b/src/TerraSharp/UI/GridBrowser.cs new file mode 100644 index 0000000..450f7be --- /dev/null +++ b/src/TerraSharp/UI/GridBrowser.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Terraria; + +namespace TerraSharp.UI; + +// Icon is a frame of Images/UI/Creative/Infinite_Icons; -1 draws the name instead. +public sealed record BrowserCategory(string Name, Func Fits, int Icon = -1, BrowserCategory[]? Subcategories = null); + +public abstract class GridBrowser(string id, string title, Vector2 position, int width, int height, string placeholder, string noun, IdIndex index, BrowserCategory[] categories) + : Window(id, title, position, width, height) +{ + const int SlotSize = 44, Gap = 4, Step = SlotSize + Gap, TabHeight = 32, SubTabHeight = 24, IconTabWidth = 38; + + readonly TextField _search = new(placeholder); + readonly Scroll _scroll = new(); + string _lastQuery = ""; + int _category, _subcategory; + readonly BrowserCategory[]?[] _subTabs = categories.Select(category => category.Subcategories?.Prepend(new BrowserCategory("All", category.Fits)).ToArray()).ToArray(); + + protected IdIndex Index { get; } = index; + + public sealed override void DrawBody(Rectangle body) + { + if (Main.gameMenu) + { + Render.TextCentered(Widgets.SpriteBatch, "Enter a world to use this.", body, Widgets.Dim, 0.9f); + return; + } + DrawContent(body); + } + + protected abstract void DrawContent(Rectangle body); + + protected abstract void DrawSlot(int id, Rectangle bounds, bool hot); + + protected void DrawSearchGrid(Rectangle body, int top, int bottom) + { + var field = new Rectangle(body.X, top, body.Width - 110, 36); + Widgets.Field(_search, field); + int gridTop = DrawCategories(body, top + 42); + Index.Filter(_search.Text, _subTabs[_category] is { } subTabs ? subTabs[_subcategory].Fits : categories[_category].Fits); + if (_search.Text != _lastQuery) + { + _lastQuery = _search.Text; + _scroll.Reset(); + } + Render.TextRight(Widgets.SpriteBatch, $"{Index.Ids.Count} {noun}", new Rectangle(field.Right, top, 106, 36), Widgets.Dim, 0.8f); + + var inner = new Rectangle(body.X, gridTop, body.Width, bottom - gridTop); + Widgets.Panel(inner, Widgets.InnerBg); + DrawGrid(Index.Ids, new Rectangle(inner.X + 10, inner.Y + 10, inner.Width - 34, inner.Height - 20)); + } + + int DrawCategories(Rectangle body, int y) + { + if (DrawTabs(body, categories, ref _category, y, TabHeight, 0.78f)) + _subcategory = 0; + y += TabHeight + 6; + if (_subTabs[_category] is not { } subTabs) + return y; + DrawTabs(body, subTabs, ref _subcategory, y, SubTabHeight, 0.68f); + return y + SubTabHeight + 6; + } + + bool DrawTabs(Rectangle body, BrowserCategory[] tabs, ref int selected, int y, int height, float scale) + { + const int TabGap = 4; + int iconTabs = tabs.Count(tab => tab.Icon >= 0); + int textTabs = tabs.Length - iconTabs; + int textWidth = textTabs == 0 ? 0 : (body.Width - TabGap * (tabs.Length - 1) - iconTabs * IconTabWidth) / textTabs; + Texture2D? icons = iconTabs > 0 ? GameTextures.Request("Images/UI/Creative/Infinite_Icons") : null; + bool changed = false; + int x = body.X; + for (int i = 0; i < tabs.Length; i++) + { + BrowserCategory tab = tabs[i]; + bool iconTab = tab.Icon >= 0; + var bounds = new Rectangle(x, y, iconTab ? IconTabWidth : textWidth, height); + x += bounds.Width + TabGap; + if (Widgets.Button(bounds, iconTab ? "" : tab.Name, i == selected, scale) && i != selected) + { + selected = i; + changed = true; + _scroll.Reset(); + } + if (!iconTab) + continue; + if (icons is not null) + { + // Journey icon frames include a 2px separator on the right. + Rectangle frame = GameTextures.Frame(icons, 11, 1, tab.Icon, 0); + frame.Width -= 2; + Widgets.Icon(icons, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), frame, 1f, i == selected || Widgets.Hot(bounds) ? Color.White : Color.White * 0.7f); + } + if (Widgets.Hot(bounds)) + Widgets.Tooltip = tab.Name; + } + return changed; + } + + void DrawGrid(List ids, Rectangle area) + { + int columns = Math.Max(1, (area.Width + Gap) / Step); + int rows = (ids.Count + columns - 1) / columns; + int y0 = _scroll.Begin(area); + int first = Math.Max(0, (area.Y - y0) / Step - 1); + int last = Math.Min(rows - 1, (area.Bottom - y0) / Step + 1); + for (int row = first; row <= last; row++) + for (int column = 0; column < columns; column++) + { + int index = row * columns + column; + if (index >= ids.Count) + break; + var bounds = new Rectangle(area.X + column * Step, y0 + row * Step, SlotSize, SlotSize); + DrawSlot(ids[index], bounds, Widgets.Hot(bounds)); + } + _scroll.End(area, rows * Step); + } +} diff --git a/src/TerraSharp/UI/IdIndex.cs b/src/TerraSharp/UI/IdIndex.cs new file mode 100644 index 0000000..26ef876 --- /dev/null +++ b/src/TerraSharp/UI/IdIndex.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace TerraSharp.UI; + +public sealed class IdIndex(Func count, Func name) +{ + string[]? _names; + string? _lastQuery; + Func? _lastFits; + + public List Ids { get; } = []; + + string[] Names + { + get + { + if (_names is not null) + return _names; + _names = new string[count()]; + for (int i = 1; i < _names.Length; i++) + _names[i] = name(i) ?? ""; + _names[0] = ""; + return _names; + } + } + + public string Name(int id) => id > 0 && id < Names.Length ? Names[id] : ""; + + public void Filter(string query, Func fits) + { + query = query.Trim(); + if (query == _lastQuery && fits == _lastFits) + return; + (_lastQuery, _lastFits) = (query, fits); + string[] names = Names; + Ids.Clear(); + if (query.Length == 0) + { + for (int i = 1; i < names.Length; i++) + if (names[i].Length > 0 && fits(i)) + Ids.Add(i); + return; + } + if (int.TryParse(query, out int id) && id > 0 && id < names.Length && names[id].Length > 0 && fits(id)) + Ids.Add(id); + foreach (bool prefixPass in (bool[])[true, false]) + for (int i = 1; i < names.Length; i++) + { + int at = names[i].IndexOf(query, StringComparison.OrdinalIgnoreCase); + if ((prefixPass ? at == 0 : at > 0) && i != id && fits(i)) + Ids.Add(i); + } + } +} diff --git a/src/TerraSharp/UI/InterfaceLayers.cs b/src/TerraSharp/UI/InterfaceLayers.cs new file mode 100644 index 0000000..74412b9 --- /dev/null +++ b/src/TerraSharp/UI/InterfaceLayers.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Terraria; +using Terraria.GameInput; +using Terraria.UI; +using TerraSharp.Core; +using TerraSharp.Visuals; + +namespace TerraSharp.UI; + +public static class InterfaceLayers +{ + const string Prefix = "TerraSharp: "; + const int OffScreen = -100000; + + static readonly FieldInfo LayersField = typeof(Main).GetField("_gameInterfaceLayers", BindingFlags.Instance | BindingFlags.NonPublic)!; + static readonly FieldInfo OriginalMouseX = typeof(PlayerInput).GetField("_originalMouseX", BindingFlags.Static | BindingFlags.NonPublic)!; + static readonly FieldInfo OriginalMouseY = typeof(PlayerInput).GetField("_originalMouseY", BindingFlags.Static | BindingFlags.NonPublic)!; + static (int X, int Y) _savedOriginal; + static bool _blocked; + + static List? Layers => LayersField.GetValue(Main.instance) as List; + + // Do not modify the layer list while DrawInterface enumerates it. + public static void Install() + { + if (Layers is not { } layers) + return; + + layers.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal)); + + layers.Insert(0, new LegacyGameInterfaceLayer(Prefix + "Under UI", DrawUnderUi, InterfaceScaleType.None)); + + int healthBarIndex = layers.FindIndex(l => l.Name == "Vanilla: Entity Health Bars"); + layers.Insert(healthBarIndex < 0 ? 1 : healthBarIndex + 1, new LegacyGameInterfaceLayer(Prefix + "Health Bars", DrawHealthBars, InterfaceScaleType.Game)); + + int mouseTextIndex = layers.FindIndex(l => l.Name == "Vanilla: Mouse Text"); + layers.Insert(mouseTextIndex < 0 ? layers.Count : mouseTextIndex, new LegacyGameInterfaceLayer(Prefix + "Windows", DrawWindows, InterfaceScaleType.UI)); + } + + public static void Uninstall() => Layers?.RemoveAll(l => l.Name.StartsWith(Prefix, StringComparison.Ordinal)); + + static bool DrawUnderUi() + { + try + { + if (Plugin.Active && !Main.gameMenu) + { + if (Settings.Esp) + Esp.Draw(Main.spriteBatch); + AimOverlay.Draw(Main.spriteBatch); + } + + Block(); + } + catch (Exception e) + { + Log.Error("Under UI layer", e); + Widgets.Recover(Main.spriteBatch, Matrix.Identity); + } + + return true; + } + + static bool DrawHealthBars() + { + try + { + if (Plugin.Active && !Main.gameMenu && Settings.Esp) + Esp.DrawHealthBars(); + } + catch (Exception e) + { + Log.Error("Health bars layer", e); + Widgets.Recover(Main.spriteBatch, Main.GameViewMatrix.ZoomMatrix); + } + + return true; + } + + static bool DrawWindows() + { + Unblock(); + PlayerInput.SetZoom_UI(); + + try + { + if (Plugin.Active && !Main.gameMenu) + WindowManager.Draw(Main.spriteBatch); + } + catch (Exception e) + { + Log.Error("Windows layer", e); + Widgets.Recover(Main.spriteBatch, Main.UIScaleMatrix); + } + + return true; + } + + // Each layer recomputes Main.mouseX/Y from PlayerInput._originalMouseX/Y. + static void Block() + { + if (!Plugin.Active || !WindowManager.AnyOpen) + return; + + int x = (int)OriginalMouseX.GetValue(null)!, y = (int)OriginalMouseY.GetValue(null)!; + if (!WindowManager.Contains(new Point((int)(x / Main.UIScale), (int)(y / Main.UIScale)))) + return; + + _savedOriginal = (x, y); + OriginalMouseX.SetValue(null, OffScreen); + OriginalMouseY.SetValue(null, OffScreen); + _blocked = true; + } + + public static void Unblock() + { + if (!_blocked) + return; + + OriginalMouseX.SetValue(null, _savedOriginal.X); + OriginalMouseY.SetValue(null, _savedOriginal.Y); + _blocked = false; + } + + // DrawMenu draws its cursor through Main.DrawCursor. + public static bool InMenuDraw { get; set; } + + static (int X, int Y)? _hiddenMouse; + + // Vanilla menus and UIState updates read Main.mouseX/Y directly. + public static void HideMouse() + { + if (_hiddenMouse is not null || !Plugin.Active || !WindowManager.MouseOver) + return; + + _hiddenMouse = GameMouse.Save(); + Main.mouseX = Main.mouseY = OffScreen; + } + + public static void ShowMouse() + { + if (_hiddenMouse is not { } saved) + return; + + GameMouse.Restore(saved); + _hiddenMouse = null; + } + + public static void DrawMenuWindows() + { + ShowMouse(); + SpriteBatch spriteBatch = Main.spriteBatch; + spriteBatch.End(); + + try + { + spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, Main.UIScaleMatrix); + WindowManager.Draw(spriteBatch); + spriteBatch.End(); + } + catch (Exception e) + { + Log.Error("Menu windows", e); + Widgets.Recover(spriteBatch, Main.UIScaleMatrix); + spriteBatch.End(); + } + + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, Main.SamplerStateForCursor, DepthStencilState.None, RasterizerState.CullCounterClockwise, null, Main.UIScaleMatrix); + } +} diff --git a/src/TerraSharp/UI/ItemBrowser.cs b/src/TerraSharp/UI/ItemBrowser.cs new file mode 100644 index 0000000..dc9bf17 --- /dev/null +++ b/src/TerraSharp/UI/ItemBrowser.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Terraria; +using Terraria.GameContent.Creative; +using Terraria.ID; +using Terraria.Localization; +using Terraria.UI; +#if TMODLOADER +using Terraria.ModLoader; +#endif + +namespace TerraSharp.UI; + +public sealed class ItemBrowser() : GridBrowser("items", "Item Browser", new Vector2(660, 120), 532, 520, "Search items by name or ID", "items", new IdIndex(() => ItemCount, Lang.GetItemNameValue), Categories()) +{ + const int InventorySlots = 50; + +#if TMODLOADER + static int ItemCount => ItemLoader.ItemCount; +#else + static int ItemCount => ItemID.Count; +#endif + + enum WeaponClass { Melee, Ranged, Magic, Summon, Other } + + static readonly IItemEntryFilter Weapon = new ItemFilters.Weapon(); + + // Journey duplication filters paired with Infinite_Icons frame indices. + static readonly (IItemEntryFilter Filter, int Icon)[] Filters = [(new ItemFilters.Tools(), 6), (new ItemFilters.Armor(), 2), (new ItemFilters.Vanity(), 8), (new ItemFilters.Accessories(), 1), (new ItemFilters.MiscAccessories(), 9), (new ItemFilters.Consumables(), 3), (new ItemFilters.BuildingBlock(), 4), (new ItemFilters.Furniture(), 7), (new ItemFilters.Materials(), 10)]; + + static BrowserCategory[] Categories() => + [ + new("All", _ => true), + new(Language.GetTextValue(Weapon.GetDisplayNameKey()), type => Fits(Weapon, type), 0, [.. Enum.GetValues(typeof(WeaponClass)).Cast().Select(weaponClass => new BrowserCategory(weaponClass.ToString(), type => ContentSample(type) is { } item && ClassOf(item) == weaponClass)), new("Ammo", type => ContentSample(type) is { ammo: > 0 })]), + .. Filters.Select(entry => new BrowserCategory(Language.GetTextValue(entry.Filter.GetDisplayNameKey()), type => Fits(entry.Filter, type), entry.Icon)), + new(Language.GetTextValue("CreativePowers.TabMisc"), type => !Fits(Weapon, type) && !Filters.Any(entry => Fits(entry.Filter, type)), 5), + ]; + + static Item? ContentSample(int type) => ContentSamples.ItemsByType.TryGetValue(type, out Item? item) ? item : null; + + // Whips must be classified as summon weapons before checking melee. + static WeaponClass? ClassOf(Item item) + { + if (!Weapon.FitsFilter(item) || item.pick > 0 || item.axe > 0 || item.hammer > 0 || item.ammo > 0) + return null; +#if TMODLOADER + if (item.CountsAsClass(DamageClass.Summon)) + return WeaponClass.Summon; + if (item.CountsAsClass(DamageClass.Magic)) + return WeaponClass.Magic; + if (item.CountsAsClass(DamageClass.Ranged)) + return WeaponClass.Ranged; + if (item.CountsAsClass(DamageClass.Melee)) + return WeaponClass.Melee; +#else + if (item.summon) + return WeaponClass.Summon; + if (item.magic) + return WeaponClass.Magic; + if (item.ranged) + return WeaponClass.Ranged; + if (item.melee) + return WeaponClass.Melee; +#endif + return WeaponClass.Other; + } + + static bool Fits(IItemEntryFilter filter, int type) => ContentSample(type) is { } item && filter.FitsFilter(item); + + readonly Dictionary _samples = []; + + public override void OnOpen() => Main.playerInventory = true; + + Item Sample(int type) + { + if (!_samples.TryGetValue(type, out Item? item)) + { + item = new Item(); + item.SetDefaults(type); + _samples[type] = item; + } + return item; + } + + protected override void DrawContent(Rectangle body) + { + DrawSearchGrid(body, body.Y, body.Bottom - 30); + Render.TextMid(Widgets.SpriteBatch, "Click: full stack to cursor Right click: +1 Shift click: to inventory", new Rectangle(body.X + 4, body.Bottom - 26, body.Width, 26), Widgets.Dim, 0.72f); + } + + protected override void DrawSlot(int type, Rectangle bounds, bool hot) + { + Widgets.Slot(bounds, hot); + Render.Item(Widgets.SpriteBatch, type, new Rectangle(bounds.X + 6, bounds.Y + 6, bounds.Width - 12, bounds.Height - 12)); + if (!hot) + return; + +#if TMODLOADER + Item sample = Sample(type); + ItemSlot.MouseHover(ref sample, ItemSlot.Context.CreativeInfinite); +#else + ItemSlot.MouseHover(Sample(type), ItemSlot.Context.CreativeInfinite); +#endif + + if (Widgets.Click) + Give(type, Widgets.Shift); + else if (Widgets.RightClick) + AddOne(type); + } + + static void Give(int type, bool toInventory) + { + if (toInventory) + { + for (int i = 0; i < InventorySlots; i++) + { + Item slot = Main.LocalPlayer.inventory[i]; + if (slot.type != 0) + continue; + + slot.SetDefaults(type); + slot.stack = slot.maxStack; + Widgets.Sound(7); + return; + } + return; + } + + if (Main.mouseItem.type != type) + { + if (Main.mouseItem.type != 0) + return; + + Main.mouseItem.SetDefaults(type); + } + + Main.mouseItem.stack = Main.mouseItem.maxStack; + + Main.playerInventory = true; + Widgets.Sound(7); + } + + static void AddOne(int type) + { + Item cursor = Main.mouseItem; + if (cursor.type == 0) + { + cursor.SetDefaults(type); + cursor.stack = 1; + } + else if (cursor.type == type && cursor.stack < cursor.maxStack) + { + cursor.stack++; + } + else + { + return; + } + + Main.playerInventory = true; + Widgets.Sound(12); + } +} diff --git a/src/TerraSharp/UI/MainWindow.Pages.cs b/src/TerraSharp/UI/MainWindow.Pages.cs new file mode 100644 index 0000000..abf29fb --- /dev/null +++ b/src/TerraSharp/UI/MainWindow.Pages.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Terraria; +using Terraria.GameContent.Events; +using TerraSharp.Core; +using TerraSharp.Features; +using TerraSharp.Features.Combat; +using TerraSharp.Simulation; + +namespace TerraSharp.UI; + +public sealed partial class MainWindow +{ + static readonly Hotkeys.Bind[] KeyBinds = [.. Hotkeys.All.GroupBy(bind => bind.Group).SelectMany(group => group.OrderBy(bind => bind.Label, StringComparer.OrdinalIgnoreCase))]; + + void PlayerPage() + { + Group("Survival"); + Toggle("God Mode", ref Settings.GodMode); + Toggle("Infinite Breath", ref Settings.InfBreath); + Toggle("Infinite Flight", ref Settings.InfFlight); + Toggle("Infinite Health", ref Settings.InfHealth); + Toggle("Infinite Mana", ref Settings.InfMana); + Toggle("Instant Respawn", ref Settings.InstantRespawn); + + Group("Potions"); + if (Toggle("Auto Heal", ref Settings.AutoHeal)) + Sub(() => Slider("Below", ref Settings.AutoHealAt, 5f, 95f, 40f, "{0:0}% health")); + if (Toggle("Auto Mana", ref Settings.AutoMana)) + Sub(() => Slider("Below", ref Settings.AutoManaAt, 5f, 95f, 25f, "{0:0}% mana")); + + Group("Actions"); + if (Buttons(WindowManager.Buffs.Open ? "Close Buff Browser" : "Buff Browser") == 0) + WindowManager.Toggle(WindowManager.Buffs); + switch (Buttons("Set Max Health to 500", "Set Max Mana to 200")) + { + case 0: PlayerFeatures.MaxHp(); break; + case 1: PlayerFeatures.MaxMana(); break; + } + switch (Buttons("Full Heal", "TP to Spawn")) + { + case 0: PlayerFeatures.FullHeal(); break; + case 1: PlayerFeatures.TeleportSpawn(); break; + } + } + + void MovementPage() + { + Group("Mobility"); + if (Toggle("Fly", ref Settings.Fly)) + Sub(() => Slider("Speed", ref Settings.FlySpeed, 2f, 40f, 12f, "{0:0.0}")); + Toggle("Gravity Flip", ref Settings.GravityFlip); + Toggle("High Jump", ref Settings.HighJump); + if (Toggle("Super Speed", ref Settings.SuperSpeed)) + Sub(() => Slider("Multiplier", ref Settings.SpeedMult, 1f, 10f, 3f, "x{0:0.0}")); + + Group("Protection"); + Toggle("Lava Immune", ref Settings.LavaImmune); + Toggle("No Fall Damage", ref Settings.NoFallDamage); + Toggle("No Knockback", ref Settings.NoKnockback); + Toggle("Water Walk", ref Settings.WaterWalk); + + Group("Reach"); + if (Toggle("Extended Reach", ref Settings.ExtendedReach)) + Sub(() => + { + SliderInt("Horizontal", ref Settings.ReachX, 5, 200, 35, "{0} tiles"); + SliderInt("Vertical", ref Settings.ReachY, 5, 200, 30, "{0} tiles"); + }); + } + + void CombatPage() + { + Group("Weapon"); + Toggle("Auto Swing", ref Settings.AutoSwing); + if (Toggle("Damage Multiplier", ref Settings.DamageBoost)) + Sub(() => Slider("Multiplier", ref Settings.DamageMult, 1f, 50f, 5f, "x{0:0.0}")); + Toggle("No Mana Cost", ref Settings.NoManaCost); + Toggle("No Spread", ref Settings.NoSpread); + Toggle("Rapid Attack", ref Settings.RapidAttack); + + Group("Aim"); + if (Toggle("Aimbot", ref Settings.Aimbot)) + Sub(() => + { + Toggle("Prediction", ref Settings.AimPrediction); + Toggle("Exact check", ref Settings.AimExact); + Slider("Cursor radius", ref Settings.AimFov, 0f, 800f, 250f, "{0:0} px", v => v < 1f ? "Anywhere" : null); + Slider("Max range", ref Settings.AimRange, 5f, 150f, 60f, "{0:0} tiles"); + Toggle("Show target", ref Settings.AimDrawTarget); + }); + if (Toggle("Auto Attack", ref Settings.AutoAttack)) + Sub(() => + { + Slider("Max range", ref Settings.AimRange, 5f, 150f, 60f, "{0:0} tiles"); + Toggle("Show target", ref Settings.AimDrawTarget); + }); + Toggle("Shot Preview", ref Settings.ShotPreview); + + Group("Enemies"); + Note("Ignores critters and target dummies."); + Toggle("Freeze Enemies", ref Settings.FreezeEnemies); + if (Toggle("Hit Magnet", ref Settings.HitMagnet)) + Sub(() => Slider("Range", ref Settings.HitMagnetRange, 1f, 150f, 30f, "{0:0} tiles")); + if (Toggle("Kill Aura", ref Settings.KillAura)) + Sub(() => + { + Slider("Range", ref Settings.KillAuraRange, 1f, 150f, 20f, "{0:0} tiles"); + SliderInt("Every", ref Settings.KillAuraInterval, 1, 60, 10, "{0} ticks"); + }); + Toggle("Weaken Enemies", ref Settings.WeakenEnemies); + switch (Buttons("Gather All Enemies", "Kill All Enemies")) + { + case 0: CombatFeatures.GatherAll(); break; + case 1: CombatFeatures.KillAll(); break; + } + } + + void ItemsPage() + { + Group("Items"); + Toggle("Infinite Items", ref Settings.InfItems); + Toggle("Instant Break", ref Settings.InstantBreak); + Toggle("Item Vacuum", ref Settings.ItemVacuum); + + Group("Fishing"); + if (Toggle("Auto Fishing", ref Settings.AutoFish)) + Sub(() => + { + Toggle("Recast", ref Settings.AutoRecast); + Toggle("Catch enemies", ref Settings.FishEnemies); + }); + Toggle("Sonar", ref Settings.Sonar); + + Group("Actions"); + if (Buttons(WindowManager.Items.Open ? "Close Item Browser" : "Item Browser") == 0) + WindowManager.Toggle(WindowManager.Items); + } + + void VisualsPage() + { + Group("ESP"); + Toggle("Enable ESP", ref Settings.Esp); + Toggle("Distance", ref Settings.EspDistance); + Toggle("Health bars", ref Settings.EspHealth); + Toggle("Health numbers", ref Settings.EspHealthText); + Slider("Max distance", ref Settings.EspMaxDistance, 0f, 400f, 0f, "{0:0} tiles", v => v < 1f ? "Unlimited" : null); + Toggle("Names", ref Settings.EspNames); + Toggle("Projectile paths", ref Settings.EspProjPath); + Choice("Style", ref Settings.EspStyle, "Outline", "Corners", "Filled"); + Slider("Thickness", ref Settings.EspThickness, 1f, 4f, 1f, "{0:0}"); + Toggle("Tracers", ref Settings.EspLines); + + Group("Targets"); + ToggleColor("Bosses (Rainbow)", ref Settings.EspBossRainbow, ref Settings.ColBoss, nameof(Settings.ColBoss)); + ToggleColor("Critters", ref Settings.EspCritters, ref Settings.ColCritters, nameof(Settings.ColCritters)); + ToggleColor("Friendly projectiles", ref Settings.EspProjFriendly, ref Settings.ColProjFriendly, nameof(Settings.ColProjFriendly)); + ToggleColor("Ground items", ref Settings.EspItems, ref Settings.ColItems, nameof(Settings.ColItems)); + ToggleColor("Hostile NPCs", ref Settings.EspHostile, ref Settings.ColHostile, nameof(Settings.ColHostile)); + ToggleColor("Hostile projectiles", ref Settings.EspProjHostile, ref Settings.ColProjHostile, nameof(Settings.ColProjHostile)); + ToggleColor("Other players", ref Settings.EspPlayers, ref Settings.ColPlayers, nameof(Settings.ColPlayers)); + ToggleColor("Town and friendly NPCs", ref Settings.EspTown, ref Settings.ColTown, nameof(Settings.ColTown)); + ToggleColor("Waypoints", ref Settings.EspWaypoints, ref Settings.ColWaypoint, nameof(Settings.ColWaypoint)); + ToggleColor("Yourself", ref Settings.EspLocal, ref Settings.ColLocal, nameof(Settings.ColLocal)); + + Group("Vision"); + Note("Potion effects without the buff."); + Toggle("Biome Sight", ref Settings.BiomeSight); + Toggle("Danger Sense", ref Settings.DangerSense); + Toggle("Hunter", ref Settings.Hunter); + Toggle("Night Owl", ref Settings.NightOwl); + Toggle("Shine", ref Settings.Shine); + Toggle("Spelunker", ref Settings.Spelunker); + } + + void WorldPage() + { + Group("Time"); + Toggle("Freeze Time", ref Settings.FreezeTime); + switch (Buttons("Dawn", "Noon", "Dusk", "Midnight")) + { + case 0: WorldFeatures.SetTime(0, true); break; + case 1: WorldFeatures.SetTime(27000, true); break; + case 2: WorldFeatures.SetTime(0, false); break; + case 3: WorldFeatures.SetTime(16200, false); break; + } + int phase = Main.moonPhase; + SliderInt("Moon", ref phase, 0, 7, 0, "{0}", v => WorldFeatures.MoonPhases[(int)Math.Round(v)]); + Main.moonPhase = phase; + + Group("Weather"); + switch (Buttons(Main.raining ? "Stop Rain" : "Start Rain", WorldFeatures.SandstormActive ? "Stop Sandstorm" : "Start Sandstorm", Main.slimeRain ? "Stop Slime Rain" : "Start Slime Rain")) + { + case 0 when Main.raining: WorldFeatures.StopRain(); break; + case 0: WorldFeatures.StartRain(); break; + case 1: WorldFeatures.SetSandstorm(!WorldFeatures.SandstormActive); break; + case 2: WorldFeatures.SetSlimeRain(!Main.slimeRain); break; + } + float wind = Main.windSpeedTarget * 100f; + Slider("Wind", ref wind, -80f, 80f, 0f, "{0:0}", v => Math.Abs(v) < 1f ? "Calm" : $"{(v < 0 ? "West" : "East")} {Math.Abs((int)v)} mph"); + if (Math.Abs(wind / 100f - Main.windSpeedTarget) > 0.0001f) + Main.windSpeedTarget = Main.windSpeedCurrent = wind / 100f; + switch (Buttons("Drop Meteor", LanternNight.LanternsUp ? "End Lantern Night" : "Lantern Night", BirthdayParty.PartyIsUp ? "End Party" : "Start Party")) + { + case 0: WorldFeatures.DropMeteor(); break; + case 1: LanternNight.ToggleManualLanterns(); break; + case 2: BirthdayParty.ToggleManualParty(); break; + } + + Group("Events"); + Note("Pumpkin and Frost Moon need nighttime."); + switch (Buttons(Main.bloodMoon ? "End Blood Moon" : "Blood Moon", Main.eclipse ? "End Eclipse" : "Solar Eclipse")) + { + case 0: Main.bloodMoon = !Main.bloodMoon; break; + case 1: Main.eclipse = !Main.eclipse; break; + } + switch (Buttons("Frost Moon", "Pumpkin Moon", "End Moon Event")) + { + case 0: Main.startSnowMoon(); break; + case 1: Main.startPumpkinMoon(); break; + case 2: Main.stopMoonEvent(); break; + } + int invasion = Buttons([.. WorldFeatures.Invasions.Select(x => x.Name)]); + if (invasion >= 0) + WorldFeatures.StartInvasion(WorldFeatures.Invasions[invasion].Id); + if (WorldFeatures.InvasionName is { } active && Buttons($"Stop {active}") == 0) + WorldFeatures.StopInvasion(); + + Group("Waypoints"); + Note("Saved per world."); + if (Buttons("Save Current Position") == 0) + Waypoints.AddHere(); + List waypoints = Waypoints.Current; + if (waypoints.Count == 0) + Note("No waypoints saved."); + for (int i = 0; i < waypoints.Count; i++) + { + var row = new Rectangle(ContentX, _y, ContentWidth, 32); + Render.TextMid(Widgets.SpriteBatch, waypoints[i].Name, row, Widgets.Hot(row) ? Widgets.Yellow : Color.White, 0.85f); + if (Widgets.Button(new Rectangle(row.Right - 168, row.Y + 2, 80, 28), "Go", false, 0.8f)) + PlayerFeatures.TeleportTo(waypoints[i].Position); + if (Widgets.Button(new Rectangle(row.Right - 82, row.Y + 2, 80, 28), "Remove", false, 0.8f)) + { + waypoints.RemoveAt(i); + break; + } + _y += 36; + } + } + + void KeysPage() + { + Note(Hotkeys.Capturing is null ? "Click a key to rebind. Right-click to clear." : "Esc cancels. Backspace or Delete clears."); + string? group = null; + foreach (Hotkeys.Bind bind in KeyBinds) + { + if (bind.Group != group) + { + group = bind.Group; + Group(group); + } + var row = new Rectangle(ContentX, _y, ContentWidth, 30); + bool capturing = Hotkeys.Capturing == bind; + Render.TextMid(Widgets.SpriteBatch, bind.Label, row, Widgets.Hot(row) ? Widgets.Yellow : Color.White, 0.86f); + var key = new Rectangle(row.Right - 150, row.Y + 1, 150, 28); + if (Widgets.Button(key, capturing ? "Press a key..." : Hotkeys.KeyName(Hotkeys.Get(bind.Id)), capturing, 0.8f)) + if (capturing) + WindowManager.Unfocus(); + else + Hotkeys.Capturing = bind; + if (Widgets.Hot(key) && Widgets.RightClick) + Hotkeys.Clear(bind.Id); + _y += 32; + } + + Group("Settings"); + Note("Auto-saved to TerraSharp.json in the Terraria save folder."); + if (Buttons("Reset Learned Ballistics") == 0) + Ballistics.Reset(); + if (Buttons(_confirmReset ? "Click again to reset everything" : "Reset All Settings") != 0) + return; + if (_confirmReset) + Settings.ResetAll(); + _confirmReset = !_confirmReset; + } +} diff --git a/src/TerraSharp/UI/MainWindow.Tooltips.cs b/src/TerraSharp/UI/MainWindow.Tooltips.cs new file mode 100644 index 0000000..4ab1e7d --- /dev/null +++ b/src/TerraSharp/UI/MainWindow.Tooltips.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using TerraSharp.Core; + +namespace TerraSharp.UI; + +public sealed partial class MainWindow +{ + static readonly Dictionary Tooltips = new() + { + ["Infinite Health"] = "Refills health every tick.", + ["Infinite Mana"] = "Refills mana every tick.", + ["Infinite Flight"] = "Unlimited wing and rocket boot flight time.", + ["Auto Heal"] = "Uses Quick Heal below the set health level. Waits for potion sickness to end.", + ["Auto Mana"] = "Uses Quick Mana below the set mana level.", + ["Fly"] = "Move through blocks with your movement keys. Up or Jump rises; Down descends.", + ["Gravity Flip"] = "Walk on the ceiling.", + ["Lava Immune"] = "Also protects against hot blocks.", + ["Water Walk"] = "Also works on honey and lava.", + ["Extended Reach"] = "Place, mine and interact with blocks farther away.", + ["Rapid Attack"] = "Speeds up item use. Hold attack with Auto Swing to keep using it.", + ["Auto Swing"] = "Hold attack to keep using any item.", + ["Damage Multiplier"] = "Applies to your held item.", + ["No Mana Cost"] = "Magic weapons cost no mana.", + ["No Spread"] = "Removes random shot spread. Shotgun pellets stay together.", + ["Aimbot"] = "Aims your shots at the enemy nearest your cursor, avoiding blocked spots.", + ["Prediction"] = "Leads moving enemies and accounts for projectile drop.", + ["Exact check"] = "Checks and corrects aim before firing. Auto Attack skips known misses. Orange brackets mean an unconfirmed shot.", + ["Shot Preview"] = "Shows your next shot's path, including spread and homing.", + ["Auto Attack"] = "Swings at enemies in reach and shoots at enemies your weapon can hit.", + ["Show target"] = "Shows the cursor radius, target brackets and aim point.", + ["Kill Aura"] = "Kills enemies in range and drops their loot.", + ["Hit Magnet"] = "Pulls the nearest enemy in range into your swing.", + ["Weaken Enemies"] = "Keeps enemies at 1 HP.", + ["Instant Break"] = "Break blocks in one hit with your held pickaxe, axe or hammer.", + ["Infinite Items"] = "Keeps inventory stacks from shrinking.", + ["Item Vacuum"] = "Pulls all dropped items to you.", + ["Auto Fishing"] = "Reels in on a bite while you hold a fishing pole.", + ["Recast"] = "Casts again where you last cast by hand.", + ["Catch enemies"] = "Reels in enemy catches too.", + ["Sonar"] = "Shows what's on the hook before you reel in.", + ["Enable ESP"] = "Marks the selected targets on screen.", + ["Health bars"] = "Also shown at full health.", + ["Health numbers"] = "Shows current and max health.", + ["Distance"] = "Shown in feet. 1 tile = 2 ft.", + ["Tracers"] = "Draws lines from you to each target.", + ["Projectile paths"] = "Shows projectile paths until they hit a block or expire.", + ["Bosses (Rainbow)"] = "Cycles boss colors. Turn off to use the chosen color.", + ["Spelunker"] = "Makes ores, gems and treasure glow.", + ["Hunter"] = "Makes enemies glow.", + ["Shine"] = "Lights the area around you.", + ["Night Owl"] = "Brightens dark areas.", + ["Danger Sense"] = "Makes traps and hazards glow.", + ["Biome Sight"] = "Makes Corruption, Crimson and Hallow blocks glow.", + ["Full Heal"] = "Refills health and mana.", + ["TP to Spawn"] = "Recalls like a Magic Mirror: to your bed if set, otherwise the world's spawn point.", + ["Reset Learned Ballistics"] = "Clears learned projectile data. Learns it again as you shoot.", + }; + + static readonly Dictionary Impacts = new() + { + ["Aimbot"] = Feature.Aimbot, + ["Prediction"] = Feature.NpcMotion, + ["Exact check"] = Feature.Aimbot, + ["Auto Attack"] = Feature.AutoAttack, + ["Shot Preview"] = Feature.ShotPreview, + ["Enable ESP"] = Feature.Esp, + ["Projectile paths"] = Feature.Esp, + }; + + static string ImpactTooltip(Feature feature) + { + Impact impact = Profiler.ImpactOf(feature); + if (impact == Impact.Idle) + return "Performance: not running."; + + float average = Profiler.Average(feature); + return $"Performance: {impact}. {average:0.000} ms per tick ({average / Profiler.FrameBudget:0.0%} of a frame), peak {Profiler.Peak(feature):0.00} ms."; + } +} diff --git a/src/TerraSharp/UI/MainWindow.cs b/src/TerraSharp/UI/MainWindow.cs new file mode 100644 index 0000000..e38dc03 --- /dev/null +++ b/src/TerraSharp/UI/MainWindow.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Terraria; +using TerraSharp.Core; + +namespace TerraSharp.UI; + +public sealed partial class MainWindow : Window +{ + sealed record Page(string Name, Action Draw, Func ActiveCount); + + const int Indent = 22; + const int TabGap = 4; + + readonly Page[] _pages; + readonly Scroll[] _scrolls; + int _page = 2; + + int _x, _y, _width, _indent; + bool _firstGroup; + + string? _openPicker; + readonly Dictionary _hsl = []; + readonly TextField _hex = new("RRGGBBAA"); + bool _confirmReset; + + public MainWindow() : base("menu", "", new Vector2(80, 120), 600, 560) + { + Open = true; + _pages = + [ + new Page("Player", PlayerPage, () => Count(Settings.GodMode, Settings.InfHealth, Settings.InfMana, Settings.InfBreath, Settings.InfFlight, Settings.InstantRespawn, Settings.AutoHeal, Settings.AutoMana)), + new Page("Movement", MovementPage, () => Count(Settings.Fly, Settings.SuperSpeed, Settings.HighJump, Settings.GravityFlip, Settings.NoFallDamage, Settings.LavaImmune, Settings.WaterWalk, Settings.NoKnockback, Settings.ExtendedReach)), + new Page("Combat", CombatPage, () => Count(Settings.RapidAttack, Settings.AutoSwing, Settings.DamageBoost, Settings.NoManaCost, Settings.NoSpread, Settings.Aimbot, Settings.AutoAttack, Settings.KillAura, Settings.HitMagnet, Settings.FreezeEnemies, Settings.WeakenEnemies)), + new Page("Items", ItemsPage, () => Count(Settings.InstantBreak, Settings.InfItems, Settings.ItemVacuum, Settings.AutoFish, Settings.Sonar)), + new Page("Visuals", VisualsPage, () => Count(Settings.Esp, Settings.Spelunker, Settings.Hunter, Settings.Shine, Settings.NightOwl, Settings.DangerSense, Settings.BiomeSight)), + new Page("World", WorldPage, () => Count(Settings.FreezeTime)), + new Page("Keys", KeysPage, () => 0), + ]; + + _scrolls = new Scroll[_pages.Length]; + for (int i = 0; i < _scrolls.Length; i++) + _scrolls[i] = new Scroll(); + } + + bool OnKeysPage => _page == _pages.Length - 1; + + static int Count(params bool[] flags) + { + int count = 0; + foreach (bool flag in flags) + if (flag) + count++; + + return count; + } + + static int ContentTop(Rectangle body) => body.Y + (Main.gameMenu ? 68 : 42); + + public override bool DragHandle(Rectangle body, Point mouse) + { + if (mouse.Y >= ContentTop(body)) + return false; + + for (int i = 0; i < _pages.Length; i++) + if (TabBounds(body, i).Contains(mouse)) + return false; + + return true; + } + + public override void DrawBody(Rectangle body) + { + DrawTabs(body); + int top = ContentTop(body); + if (Main.gameMenu) + Render.TextMid(Widgets.SpriteBatch, "Enter a world to use actions.", new Rectangle(body.X + 4, body.Y + 42, body.Width, 24), Widgets.Yellow * 0.9f, 0.74f); + + var inner = new Rectangle(body.X, top, body.Width, body.Bottom - top); + Widgets.Panel(inner, Widgets.InnerBg); + var area = new Rectangle(inner.X + 12, inner.Y + 8, inner.Width - 38, inner.Height - 16); + + Scroll scroll = _scrolls[_page]; + _y = scroll.Begin(area); + int start = _y; + (_x, _width, _indent, _firstGroup) = (area.X, area.Width, 0, true); + _pages[_page].Draw(); + scroll.End(area, _y - start + 4); + } + + Rectangle TabBounds(Rectangle body, int index) + { + int tabWidth = (body.Width - TabGap * (_pages.Length - 1)) / _pages.Length; + return new Rectangle(body.X + index * (tabWidth + TabGap), body.Y, tabWidth, 34); + } + + void DrawTabs(Rectangle body) + { + for (int i = 0; i < _pages.Length; i++) + { + Rectangle bounds = TabBounds(body, i); + if (!Widgets.Button(bounds, _pages[i].Name, i == _page, 0.78f) || _page == i) + continue; + + _page = i; + _openPicker = null; + WindowManager.Unfocus(); + } + + for (int i = 0; i < _pages.Length; i++) + if (_pages[i].ActiveCount() is var count and > 0) + Widgets.Badge(TabBounds(body, i).Right - 6, body.Y + 2, count.ToString()); + } + + int ContentX => _x + _indent; + + int ContentWidth => _width - _indent; + + void Sub(Action body) + { + _indent += Indent; + body(); + _indent -= Indent; + } + + void Group(string title) + { + if (!_firstGroup) + _y += 12; + _firstGroup = false; + Widgets.Text(title, new Vector2(ContentX, _y + 2), Widgets.Header, 0.92f); + float width = Render.Measure(title, 0.92f).X; + Widgets.Separator(ContentX + (int)width + 12, _y + 12, ContentWidth - (int)width - 12); + _y += 30; + } + + void Note(string text) + { + Widgets.Text(text, new Vector2(ContentX, _y + 2), Widgets.Dim * 0.8f, 0.72f); + _y += 24; + } + + bool Toggle(string label, ref bool value, int rightPad = 0) + { + var row = new Rectangle(ContentX, _y, ContentWidth, 28); + bool hot = Widgets.Hot(new Rectangle(row.X, row.Y, row.Width - rightPad, row.Height)); + if (hot && Widgets.Click) + { + value = !value; + Widgets.Sound(12); + } + + if (hot) + { + Render.FillSoft(Widgets.SpriteBatch, new Rectangle(row.X - 6, row.Y, row.Width + 6, row.Height - 2), Color.White * 0.05f); + if (Tooltips.TryGetValue(label, out string? tooltip)) + Widgets.Tooltip = tooltip; + } + + Color color = hot ? Widgets.Yellow : value ? Widgets.On : _indent > 0 ? Widgets.Dim : Color.White; + Render.TextMid(Widgets.SpriteBatch, label, row, color, 0.88f); + Widgets.ToggleIcon(new Rectangle(row.Right - 28, row.Y + 2, 24, 24), value, hot); + if (Impacts.TryGetValue(label, out Feature feature)) + ImpactDot(new Rectangle(row.Right - rightPad - 82, row.Y + 10, 8, 8), feature); + _y += 30; + return value; + } + + static void ImpactDot(Rectangle bounds, Feature feature) + { + Widgets.ImpactDot(bounds, Profiler.ImpactOf(feature)); + if (Widgets.Hot(new Rectangle(bounds.X - 6, bounds.Y - 9, bounds.Width + 12, bounds.Height + 18))) + Widgets.Tooltip = ImpactTooltip(feature); + } + + void ToggleColor(string label, ref bool value, ref Color color, string settingName) + { + int top = _y; + Toggle(label, ref value, 110); + var swatch = new Rectangle(ContentX + ContentWidth - 104, top + 3, 34, 22); + bool hot = Widgets.Hot(swatch); + bool open = _openPicker == settingName; + Widgets.Panel(swatch, Widgets.ButtonBg, hot || open ? Widgets.Yellow : Color.Black); + Render.Fill(Widgets.SpriteBatch, new Rectangle(swatch.X + 5, swatch.Y + 5, swatch.Width - 10, swatch.Height - 10), new Color(color.R, color.G, color.B) * (color.A / 255f)); + + if (hot) + Widgets.Tooltip = "Edit color"; + if (hot && Widgets.Click) + { + _openPicker = open ? null : settingName; + WindowManager.Unfocus(); + Widgets.Sound(12); + } + + if (_openPicker != settingName) + return; + + _indent += Indent; + ColorPicker(settingName, ref color); + _indent -= Indent; + } + + static readonly Color[] Presets = [new(235, 60, 60), new(255, 140, 40), new(255, 210, 70), new(120, 230, 90), new(70, 220, 200), new(70, 200, 255), new(90, 110, 255), new(200, 90, 255), new(255, 110, 200), Color.White]; + + void ColorPicker(string settingName, ref Color color) + { + // Achromatic colors cannot recover the picker's previous hue from RGB. + Vector3 hsl = Main.rgbToHsl(color); + if (_hsl.TryGetValue(settingName, out Vector3 cached) && Main.hslToRgb(cached.X, cached.Y, cached.Z).PackedValue == new Color(color.R, color.G, color.B).PackedValue) + hsl = cached; + + float hue = hsl.X, saturation = hsl.Y, lightness = hsl.Z, opacity = color.A / 255f; + Color rgb = Main.hslToRgb(hue, saturation, lightness); + hue = GradientSlider("Hue", settingName + "h", hue, p => Main.hslToRgb(p, 1f, 0.5f)); + saturation = GradientSlider("Saturation", settingName + "s", saturation, p => Main.hslToRgb(hue, p, lightness)); + lightness = GradientSlider("Lightness", settingName + "l", lightness, p => Main.hslToRgb(hue, saturation, p)); + opacity = GradientSlider("Opacity", settingName + "a", opacity, p => Color.Lerp(Color.Black, rgb, p)); + _hsl[settingName] = new Vector3(hue, saturation, lightness); + color = Main.hslToRgb(hue, saturation, lightness, (byte)(opacity * 255)); + + int size = Math.Min(30, (ContentWidth - 6 * (Presets.Length - 1)) / Presets.Length); + for (int i = 0; i < Presets.Length; i++) + { + var bounds = new Rectangle(ContentX + i * (size + 6), _y + 2, size, size); + bool hot = Widgets.Hot(bounds); + Widgets.Slot(bounds, hot); + Render.Fill(Widgets.SpriteBatch, new Rectangle(bounds.X + 6, bounds.Y + 6, bounds.Width - 12, bounds.Height - 12), Presets[i]); + if (!hot || !Widgets.Click) + continue; + + color = new Color(Presets[i].R, Presets[i].G, Presets[i].B, color.A); + _hsl.Remove(settingName); + Widgets.Sound(12); + } + _y += size + 8; + + if (WindowManager.Focus != _hex) + _hex.Text = Settings.Hex(color); + Widgets.Field(_hex, new Rectangle(ContentX, _y + 2, ContentWidth - 120, 32)); + if (WindowManager.Focus == _hex) + { + try + { + color = Settings.ParseHex(_hex.Text); + _hsl.Remove(settingName); + } + catch (FormatException) + { + } + } + + if (Widgets.Button(new Rectangle(ContentX + ContentWidth - 112, _y + 2, 112, 32), "Default", false, 0.8f) && Settings.Default(settingName) is Color fallback) + { + color = fallback; + _hsl.Remove(settingName); + } + _y += 42; + } + + float GradientSlider(string label, string id, float value, Utils.ColorLerpMethod gradient) + { + var row = new Rectangle(ContentX, _y, ContentWidth, 26); + bool hot = Widgets.Hot(row) || Widgets.ActiveSlider == id; + Render.TextMid(Widgets.SpriteBatch, label, row, hot ? Widgets.Yellow : Widgets.Dim, 0.8f); + value = Widgets.ValueBar(id, row, value, 0f, 1f, value, gradient); + _y += 28; + return value; + } + + void Choice(string label, ref T value, params string[] options) where T : struct, Enum + { + var row = new Rectangle(ContentX, _y, ContentWidth, 30); + Render.TextMid(Widgets.SpriteBatch, label, row, Widgets.Hot(row) ? Widgets.Yellow : Widgets.Dim, 0.85f); + const int ButtonWidth = 92, Gap = 4; + int x0 = row.Right - options.Length * ButtonWidth - (options.Length - 1) * Gap; + int selected = Convert.ToInt32(value); + for (int i = 0; i < options.Length; i++) + if (Widgets.Button(new Rectangle(x0 + i * (ButtonWidth + Gap), row.Y + 1, ButtonWidth, 28), options[i], selected == i, 0.76f)) + value = (T)Enum.ToObject(typeof(T), i); + + _y += 34; + } + + void Slider(string label, ref float value, float minimum, float maximum, float fallback, string format, Func? display = null) + { + var row = new Rectangle(ContentX, _y, ContentWidth, 28); + string id = label + "|" + format; + bool hot = Widgets.Hot(row) || Widgets.ActiveSlider == id; + + if (hot) + Widgets.Tooltip = "Drag to change. Right-click to reset."; + + string shown = display?.Invoke(value) ?? string.Format(format, value); + Render.TextMid(Widgets.SpriteBatch, $"{label}: {shown}", row, hot ? Widgets.Yellow : Widgets.Dim, 0.85f); + value = Widgets.ValueBar(id, row, value, minimum, maximum, fallback); + _y += 30; + } + + void SliderInt(string label, ref int value, int minimum, int maximum, int fallback, string format, Func? display = null) + { + float floatValue = value; + Slider(label, ref floatValue, minimum, maximum, fallback, format, display); + value = (int)Math.Round(floatValue); + } + + int Buttons(params string[] labels) + { + const int Gap = 6; + int width = (ContentWidth - Gap * (labels.Length - 1)) / labels.Length; + bool enabled = !Main.gameMenu || OnKeysPage; + int hit = -1; + for (int i = 0; i < labels.Length; i++) + { + var bounds = new Rectangle(ContentX + i * (width + Gap), _y + 2, width, 30); + if (Widgets.Button(bounds, labels[i], false, 0.82f, enabled)) + hit = i; + if (enabled && Widgets.Hot(bounds) && Tooltips.TryGetValue(labels[i], out string? tooltip)) + Widgets.Tooltip = tooltip; + } + + _y += 36; + return hit; + } +} diff --git a/src/TerraSharp/UI/Render.cs b/src/TerraSharp/UI/Render.cs new file mode 100644 index 0000000..d39bad9 --- /dev/null +++ b/src/TerraSharp/UI/Render.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Terraria; +using Terraria.GameContent; + +namespace TerraSharp.UI; + +public static class Render +{ + static Texture2D? _pixel; + + static Texture2D Pixel + { + get + { + if (_pixel is null || _pixel.IsDisposed) + { + _pixel = new Texture2D(Main.instance.GraphicsDevice, 1, 1); + _pixel.SetData([Color.White]); + } + return _pixel; + } + } + + public static void Fill(SpriteBatch spriteBatch, Rectangle bounds, Color color) => spriteBatch.Draw(Pixel, bounds, color); + + public static void Outline(SpriteBatch spriteBatch, Rectangle bounds, Color color, int thickness = 1) + { + Fill(spriteBatch, new Rectangle(bounds.X, bounds.Y, bounds.Width, thickness), color); + Fill(spriteBatch, new Rectangle(bounds.X, bounds.Bottom - thickness, bounds.Width, thickness), color); + Fill(spriteBatch, new Rectangle(bounds.X, bounds.Y, thickness, bounds.Height), color); + Fill(spriteBatch, new Rectangle(bounds.Right - thickness, bounds.Y, thickness, bounds.Height), color); + } + + public static void FillSoft(SpriteBatch spriteBatch, Rectangle bounds, Color color) + { + Fill(spriteBatch, new Rectangle(bounds.X + 1, bounds.Y, bounds.Width - 2, bounds.Height), color); + Fill(spriteBatch, new Rectangle(bounds.X, bounds.Y + 1, 1, bounds.Height - 2), color); + Fill(spriteBatch, new Rectangle(bounds.Right - 1, bounds.Y + 1, 1, bounds.Height - 2), color); + } + + public static void Line(SpriteBatch spriteBatch, Vector2 start, Vector2 end, Color color, float thickness = 1f) + { + Vector2 delta = end - start; + spriteBatch.Draw(Pixel, start, null, color, (float)Math.Atan2(delta.Y, delta.X), new Vector2(0f, 0.5f), new Vector2(delta.Length(), thickness), SpriteEffects.None, 0f); + } + + public static void Corners(SpriteBatch spriteBatch, Vector2 topLeft, Vector2 bottomRight, Color color, float thickness) + { + float cornerWidth = Math.Max(4f, (bottomRight.X - topLeft.X) / 4f), cornerHeight = Math.Max(4f, (bottomRight.Y - topLeft.Y) / 4f); + Vector2 topRight = new(bottomRight.X, topLeft.Y), bottomLeft = new(topLeft.X, bottomRight.Y); + Line(spriteBatch, topLeft, topLeft + new Vector2(cornerWidth, 0), color, thickness); + Line(spriteBatch, topLeft, topLeft + new Vector2(0, cornerHeight), color, thickness); + Line(spriteBatch, topRight, topRight - new Vector2(cornerWidth, 0), color, thickness); + Line(spriteBatch, topRight, topRight + new Vector2(0, cornerHeight), color, thickness); + Line(spriteBatch, bottomLeft, bottomLeft + new Vector2(cornerWidth, 0), color, thickness); + Line(spriteBatch, bottomLeft, bottomLeft - new Vector2(0, cornerHeight), color, thickness); + Line(spriteBatch, bottomRight, bottomRight - new Vector2(cornerWidth, 0), color, thickness); + Line(spriteBatch, bottomRight, bottomRight - new Vector2(0, cornerHeight), color, thickness); + } + + public static void TextCenteredAt(SpriteBatch spriteBatch, string text, float centerX, float y, Color color, float scale) => Text(spriteBatch, text, new Vector2(centerX - Measure(text, scale).X / 2f, y), color, scale); + + // FontAssets.MouseText is a ReLogic type and must be accessed through reflection. + static FieldInfo? _fontField; + static PropertyInfo? _assetValue; + static MethodInfo? _measureString; + static object? _font; + static readonly Dictionary Sizes = []; + + static Vector2 MeasureRaw(string text) + { + if (_measureString is null) + { + _fontField = typeof(FontAssets).GetField("MouseText", BindingFlags.Public | BindingFlags.Static)!; + _assetValue = _fontField.FieldType.GetProperty("Value")!; + _measureString = _assetValue.PropertyType.GetMethod("MeasureString", [typeof(string)])!; + } + + object font = _assetValue!.GetValue(_fontField!.GetValue(null), null)!; + // A resource pack can swap the font. + if (!ReferenceEquals(font, _font)) + { + _font = font; + Sizes.Clear(); + } + + if (Sizes.TryGetValue(text, out Vector2 size)) + return size; + + size = (Vector2)_measureString.Invoke(font, [text])!; + if (Sizes.Count > 4096) + Sizes.Clear(); + Sizes[text] = size; + return size; + } + + public static Vector2 Measure(string text, float scale) => string.IsNullOrEmpty(text) ? Vector2.Zero : MeasureRaw(text) * scale; + + // Visual line height; the font reports a large descent. + public static float LineHeight(float scale) => 22f * scale; + + static float CenterY(Rectangle bounds, float scale) => bounds.Y + (bounds.Height - LineHeight(scale)) / 2f + 2f * scale; + + public static void Text(SpriteBatch spriteBatch, string text, Vector2 position, Color color, float scale) + { + if (!string.IsNullOrEmpty(text)) + Utils.DrawBorderString(spriteBatch, text, position, color, scale, 0f, 0f, -1); + } + + public static void TextCentered(SpriteBatch spriteBatch, string text, Rectangle bounds, Color color, float scale) => Text(spriteBatch, text, new Vector2(bounds.X + (bounds.Width - Measure(text, scale).X) / 2f, CenterY(bounds, scale)), color, scale); + + public static void TextMid(SpriteBatch spriteBatch, string text, Rectangle bounds, Color color, float scale) => Text(spriteBatch, text, new Vector2(bounds.X, CenterY(bounds, scale)), color, scale); + + public static void TextRight(SpriteBatch spriteBatch, string text, Rectangle bounds, Color color, float scale) => Text(spriteBatch, text, new Vector2(bounds.Right - Measure(text, scale).X, CenterY(bounds, scale)), color, scale); + + public static string Ellipsize(string text, float scale, float maxWidth) + { + if (Measure(text, scale).X <= maxWidth) + return text; + + while (text.Length > 1 && Measure(text + "...", scale).X > maxWidth) + text = text.Substring(0, text.Length - 1); + return text + "..."; + } + + public static void Item(SpriteBatch spriteBatch, int type, Rectangle bounds) + { + if (type <= 0) + return; + + Main.instance.LoadItem(type); + Main.GetItemDrawFrame(type, out Texture2D texture, out Rectangle frame); + if (texture is null) + return; + + float scale = Math.Min(1f, Math.Min(bounds.Width / (float)frame.Width, bounds.Height / (float)frame.Height)); + Vector2 size = new Vector2(frame.Width, frame.Height) * scale; + spriteBatch.Draw(texture, new Vector2(bounds.X + (bounds.Width - size.X) / 2f, bounds.Y + (bounds.Height - size.Y) / 2f), frame, Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f); + } + + public static Vector2 ToScreen(Vector2 world) => Vector2.Transform(world - Main.screenPosition, Main.GameViewMatrix.TransformationMatrix); +} diff --git a/src/TerraSharp/UI/Scroll.cs b/src/TerraSharp/UI/Scroll.cs new file mode 100644 index 0000000..8a73170 --- /dev/null +++ b/src/TerraSharp/UI/Scroll.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.Xna.Framework; + +namespace TerraSharp.UI; + +public sealed class Scroll +{ + float _position, _target; + int _contentHeight; + float? _grab; + + public float Position => _position; + + public int Begin(Rectangle area) + { + int maximum = Math.Max(0, _contentHeight - area.Height); + if (Widgets.Hot(area) && Widgets.Wheel != 0 && Widgets.ActiveSlider is null) + _target -= Widgets.Wheel / 120f * 60f; + _target = MathHelper.Clamp(_target, 0, maximum); + _position = MathHelper.Lerp(_position, _target, 0.35f); + if (Math.Abs(_position - _target) < 0.5f) + _position = _target; + + Widgets.BeginClip(area); + return area.Y - (int)_position; + } + + public void End(Rectangle area, int contentHeight) + { + _contentHeight = contentHeight; + Widgets.EndClip(); + if (!Widgets.Down) + _grab = null; + if (_contentHeight <= area.Height) + return; + + int maximum = _contentHeight - area.Height; + var track = new Rectangle(area.Right + 4, area.Y, 16, area.Height); + Widgets.ScrollBar("Images/UI/Scrollbar", track, Color.White); + int thumb = Math.Max(28, (track.Height - 8) * area.Height / _contentHeight); + int travel = track.Height - 8 - thumb; + int y = track.Y + 4 + (int)(travel * (_position / maximum)); + bool hot = Widgets.Hot(track); + + if (hot && Widgets.Click && Widgets.ActiveSlider is null) + _grab = Widgets.Mouse.Y >= y && Widgets.Mouse.Y < y + thumb ? Widgets.Mouse.Y - y : thumb / 2f; + if (_grab is { } grab && travel > 0) + { + _position = _target = MathHelper.Clamp((Widgets.Mouse.Y - grab - track.Y - 4) / travel * maximum, 0, maximum); + y = track.Y + 4 + (int)(travel * (_position / maximum)); + } + + Widgets.ScrollBar("Images/UI/ScrollbarInner", new Rectangle(track.X + 3, y, track.Width - 6, thumb), Color.White * (hot || _grab is not null ? 1f : 0.85f)); + } + + public void Reset() => _position = _target = 0; +} diff --git a/src/TerraSharp/UI/TextField.cs b/src/TerraSharp/UI/TextField.cs new file mode 100644 index 0000000..fc3ff8e --- /dev/null +++ b/src/TerraSharp/UI/TextField.cs @@ -0,0 +1,8 @@ +namespace TerraSharp.UI; + +public sealed class TextField(string placeholder) +{ + public string Text { get; set; } = ""; + + public string Placeholder { get; } = placeholder; +} diff --git a/src/TerraSharp/UI/Widgets.cs b/src/TerraSharp/UI/Widgets.cs new file mode 100644 index 0000000..48a00e1 --- /dev/null +++ b/src/TerraSharp/UI/Widgets.cs @@ -0,0 +1,334 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using Terraria; +using Terraria.Audio; +using TerraSharp.Core; +#if TMODLOADER +using Microsoft.Xna.Framework.Audio; +using TerraSharp.Patches; +#endif + +namespace TerraSharp.UI; + +public static class Widgets +{ + public static readonly Color Yellow = new(255, 231, 69); // Main.OurFavoriteColor + public static readonly Color WindowBg = new Color(33, 43, 79) * 0.96f; // Bestiary / Journey main panel + public static readonly Color InnerBg = new Color(22, 28, 56) * 0.9f; + public static readonly Color ButtonBg = new Color(63, 82, 151) * 0.7f; // UIPanel default + public static readonly Color ButtonHot = new(73, 94, 171); + public static readonly Color Header = new(205, 215, 255); + public static readonly Color On = new(120, 235, 120); + public static readonly Color Dim = new(175, 175, 200); + public static readonly Color Orange = new(255, 150, 40); + public static readonly Color Red = new(235, 60, 60); + static readonly Color SlotTint = Color.White * 0.86f; + static readonly Rectangle NoClip = new(-100000, -100000, 200000, 200000); + + public static SpriteBatch SpriteBatch { get; set; } = null!; + public static Point Mouse { get; private set; } + public static bool InputOk { get; private set; } + public static bool Down { get; private set; } + public static bool Click { get; private set; } + public static bool RightClick { get; private set; } + public static bool Shift { get; private set; } + public static int Wheel { get; private set; } + public static bool Blocked { get; set; } + public static Rectangle Clip { get; private set; } = NoClip; + + // Vanilla OverflowHidden uses the same device scissor rectangle. + static Rectangle _gameScissor; + public static string? Tooltip { get; set; } + public static string? ActiveSlider { get; private set; } + + static MouseState _state, _previous; + static readonly RasterizerState Scissor = new() { CullMode = CullMode.None, ScissorTestEnable = true }; + static readonly Stopwatch Clock = Stopwatch.StartNew(); + + public static double Now => Clock.Elapsed.TotalSeconds; + + // Position from Main.mouseX/Y (UI space inside a UI layer); buttons from raw XNA state. + public static void Poll() + { + _previous = _state; + _state = Microsoft.Xna.Framework.Input.Mouse.GetState(); + Mouse = new Point(Main.mouseX, Main.mouseY); + InputOk = Main.instance.IsActive; + Down = InputOk && _state.LeftButton == ButtonState.Pressed; + Click = Down && _previous.LeftButton == ButtonState.Released; + RightClick = InputOk && _state.RightButton == ButtonState.Pressed && _previous.RightButton == ButtonState.Released; + Wheel = _state.ScrollWheelValue - _previous.ScrollWheelValue; + + KeyboardState keyboard = Keyboard.GetState(); + Shift = keyboard.IsKeyDown(Keys.LeftShift) || keyboard.IsKeyDown(Keys.RightShift); + + Tooltip = null; + if (!Down) + ActiveSlider = null; + } + + public static bool Hot(Rectangle bounds) => InputOk && !Blocked && Clip.Contains(Mouse) && bounds.Contains(Mouse); + +#if TMODLOADER + // tModLoader keeps the legacy id overload internal; the public ones return ReLogic's SlotId. + static readonly Func PlayLegacy = (Func)Delegate.CreateDelegate(typeof(Func), HookRegistry.Method(typeof(SoundEngine), nameof(SoundEngine.PlaySound), [typeof(int), typeof(int), typeof(int), typeof(int), typeof(float), typeof(float)])); + + public static void Sound(int type, int x = -1, int y = -1, int style = 1) => PlayLegacy(type, x, y, style, 1f, 0f); +#else + public static void Sound(int type, int x = -1, int y = -1, int style = 1) => SoundEngine.PlaySound(type, x, y, style, 1f, 0f); +#endif + + public static void Panel(Rectangle bounds, Color background, Color? border = null) + { + Texture2D? back = GameTextures.Request("Images/UI/PanelBackground"); + if (back is null) + { + Utils.DrawInvBG(SpriteBatch, bounds, background); + return; + } + + NineSlice(back, bounds, background, 12, 4); + if (GameTextures.Request("Images/UI/PanelBorder") is { } edge) + NineSlice(edge, bounds, border ?? Color.Black, 12, 4); + } + + static void NineSlice(Texture2D texture, Rectangle bounds, Color color, int corner, int bar) + { + bounds.Width = Math.Max(bounds.Width, corner * 2); + bounds.Height = Math.Max(bounds.Height, corner * 2); + int x2 = bounds.Right - corner, y2 = bounds.Bottom - corner, width = bounds.Width - corner * 2, height = bounds.Height - corner * 2, far = corner + bar; + + SpriteBatch.Draw(texture, new Rectangle(bounds.X, bounds.Y, corner, corner), new Rectangle(0, 0, corner, corner), color); + SpriteBatch.Draw(texture, new Rectangle(x2, bounds.Y, corner, corner), new Rectangle(far, 0, corner, corner), color); + SpriteBatch.Draw(texture, new Rectangle(bounds.X, y2, corner, corner), new Rectangle(0, far, corner, corner), color); + SpriteBatch.Draw(texture, new Rectangle(x2, y2, corner, corner), new Rectangle(far, far, corner, corner), color); + SpriteBatch.Draw(texture, new Rectangle(bounds.X + corner, bounds.Y, width, corner), new Rectangle(corner, 0, bar, corner), color); + SpriteBatch.Draw(texture, new Rectangle(bounds.X + corner, y2, width, corner), new Rectangle(corner, far, bar, corner), color); + SpriteBatch.Draw(texture, new Rectangle(bounds.X, bounds.Y + corner, corner, height), new Rectangle(0, corner, corner, bar), color); + SpriteBatch.Draw(texture, new Rectangle(x2, bounds.Y + corner, corner, height), new Rectangle(far, corner, corner, bar), color); + SpriteBatch.Draw(texture, new Rectangle(bounds.X + corner, bounds.Y + corner, width, height), new Rectangle(corner, corner, bar, bar), color); + } + + public static void ScrollBar(string path, Rectangle bounds, Color color) + { + if (GameTextures.Request(path) is not { } texture) + { + Render.Fill(SpriteBatch, bounds, color); + return; + } + + const int CapHeight = 6; + SpriteBatch.Draw(texture, new Rectangle(bounds.X, bounds.Y, bounds.Width, CapHeight), new Rectangle(0, 0, texture.Width, CapHeight), color); + SpriteBatch.Draw(texture, new Rectangle(bounds.X, bounds.Y + CapHeight, bounds.Width, Math.Max(0, bounds.Height - CapHeight * 2)), new Rectangle(0, CapHeight, texture.Width, 4), color); + SpriteBatch.Draw(texture, new Rectangle(bounds.X, bounds.Bottom - CapHeight, bounds.Width, CapHeight), new Rectangle(0, texture.Height - CapHeight, texture.Width, CapHeight), color); + } + + // Scissor rectangles are in backbuffer pixels; ours are UI units. + public static void BeginClip(Rectangle bounds) + { + GraphicsDevice graphicsDevice = Main.instance.GraphicsDevice; + float scale = Main.UIScale; + var pixels = new Rectangle((int)(bounds.X * scale), (int)(bounds.Y * scale), (int)Math.Ceiling(bounds.Width * scale), (int)Math.Ceiling(bounds.Height * scale)); + + SpriteBatch.End(); + _gameScissor = graphicsDevice.ScissorRectangle; + graphicsDevice.ScissorRectangle = Rectangle.Intersect(pixels, graphicsDevice.Viewport.Bounds); + SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, Scissor, null, Main.UIScaleMatrix); + Clip = bounds; + } + + public static void EndClip() + { + SpriteBatch.End(); + + RestoreScissor(); + SpriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, Main.UIScaleMatrix); + } + + static void RestoreScissor() + { + if (Clip == NoClip) + return; + + Main.instance.GraphicsDevice.ScissorRectangle = _gameScissor; + Clip = NoClip; + } + +#if TMODLOADER + static readonly FieldInfo? InBeginEndPair = typeof(SpriteBatch).GetField("beginCalled", BindingFlags.Instance | BindingFlags.NonPublic); + static readonly FieldInfo? SpriteQueueCount = typeof(SpriteBatch).GetField("numSprites", BindingFlags.Instance | BindingFlags.NonPublic); +#else + static readonly FieldInfo? InBeginEndPair = typeof(SpriteBatch).GetField("inBeginEndPair", BindingFlags.Instance | BindingFlags.NonPublic); + static readonly FieldInfo? SpriteQueueCount = typeof(SpriteBatch).GetField("spriteQueueCount", BindingFlags.Instance | BindingFlags.NonPublic); +#endif + + // A failed Flush leaves the batch begun and its sprite queue intact. + public static void Recover(SpriteBatch spriteBatch, Matrix matrix) + { + try + { + if (InBeginEndPair?.GetValue(spriteBatch) is true) + { + SpriteQueueCount?.SetValue(spriteBatch, 0); + spriteBatch.End(); + } + } + catch (Exception e) + { + Log.Error("Recover", e); + } + + RestoreScissor(); + spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, matrix); + } + + public static void Text(string text, Vector2 position, Color color, float scale = 0.9f) => Render.Text(SpriteBatch, text, position, color, scale); + + public static void Separator(int x, int y, int width) + { + Render.Fill(SpriteBatch, new Rectangle(x, y, width, 2), Color.Black * 0.45f); + Render.Fill(SpriteBatch, new Rectangle(x, y + 2, width, 1), Color.White * 0.08f); + } + + public static void Badge(int centerX, int centerY, string text) + { + float width = Math.Max(18, Render.Measure(text, 0.6f).X + 10); + var bounds = new Rectangle((int)(centerX - width / 2), centerY - 9, (int)width, 18); + Render.FillSoft(SpriteBatch, new Rectangle(bounds.X - 1, bounds.Y - 1, bounds.Width + 2, bounds.Height + 2), Color.Black * 0.8f); + Render.FillSoft(SpriteBatch, bounds, new Color(60, 150, 60)); + Render.TextCentered(SpriteBatch, text, bounds, Color.White, 0.6f); + } + + public static bool Button(Rectangle bounds, string label, bool selected = false, float scale = 0.85f, bool enabled = true) + { + string text = Render.Ellipsize(label, scale, bounds.Width - 12); + if (!enabled) + { + Panel(bounds, ButtonBg * 0.5f, Color.Black * 0.6f); + Render.TextCentered(SpriteBatch, text, bounds, Dim * 0.5f, scale); + if (Hot(bounds)) + Tooltip = "Only available in-game"; + return false; + } + + bool hot = Hot(bounds); + bool lit = selected || hot; + Panel(bounds, lit ? ButtonHot : ButtonBg, lit ? Yellow : Color.Black); + Render.TextCentered(SpriteBatch, text, bounds, lit ? Yellow : Color.White, scale); + + if (!hot || !Click) + return false; + + Sound(12); + return true; + } + + public static void ToggleIcon(Rectangle bounds, bool on, bool hot) + { + Render.TextRight(SpriteBatch, on ? "On" : "Off", new Rectangle(bounds.X - 44, bounds.Y, 38, bounds.Height), on ? On : Dim * 0.75f, 0.78f); + if (GameTextures.Request("Images/UI/Settings_Toggle") is not { } texture) + { + if (GameTextures.Get(on ? "InventoryTickOn" : "InventoryTickOff") is { } tick) + SpriteBatch.Draw(tick, bounds, Color.White); + return; + } + + Icon(texture, bounds, GameTextures.Frame(texture, 2, 1, on ? 1 : 0, 0), 100f, on ? On : hot ? Yellow : Color.White * 0.7f); + } + + public static void ImpactDot(Rectangle bounds, Impact impact) + { + Color color = impact switch { Impact.Negligible => On, Impact.Low => Yellow, Impact.Medium => Orange, Impact.High => Red, _ => Dim * 0.35f }; + Render.Fill(SpriteBatch, bounds, Color.Black * 0.6f); + Render.Fill(SpriteBatch, new Rectangle(bounds.X + 1, bounds.Y + 1, bounds.Width - 2, bounds.Height - 2), color); + } + + public static void Slot(Rectangle bounds, bool hot, bool selected = false) + { + if (GameTextures.Get(selected ? "InventoryBack14" : hot ? "InventoryBack15" : "InventoryBack") is { } back) + SpriteBatch.Draw(back, bounds, SlotTint); + else + Panel(bounds, ButtonBg); + } + + public static void Icon(Texture2D? texture, Rectangle bounds, Rectangle? frame = null, float maxScale = 1f, Color? color = null) + { + if (texture is null) + return; + + Rectangle source = frame ?? texture.Bounds; + float scale = Math.Min(maxScale, Math.Min(bounds.Width / (float)source.Width, bounds.Height / (float)source.Height)); + var size = new Vector2(source.Width, source.Height) * scale; + SpriteBatch.Draw(texture, new Vector2(bounds.X + (bounds.Width - size.X) / 2f, bounds.Y + (bounds.Height - size.Y) / 2f), source, color ?? Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f); + } + + public static void Field(TextField field, Rectangle bounds) + { + bool focused = WindowManager.Focus == field; + if (Hot(bounds) && Click && !focused) + { + WindowManager.Focus = field; + focused = true; + Main.clrInput(); + + Sound(12); + } + + Panel(bounds, focused ? ButtonHot : InnerBg, focused ? Yellow : Color.Black); + Icon(GameTextures.Request("Images/UI/Bestiary/Button_Search"), new Rectangle(bounds.X + 6, bounds.Y + 4, bounds.Height - 8, bounds.Height - 8)); + var textArea = new Rectangle(bounds.X + bounds.Height + 2, bounds.Y, bounds.Width - bounds.Height - 30, bounds.Height); + if (field.Text.Length == 0 && !focused) + Render.TextMid(SpriteBatch, field.Placeholder, textArea, Dim * 0.8f, 0.85f); + else + Render.TextMid(SpriteBatch, field.Text + (focused && (int)(Now * 2.5) % 2 == 0 ? "|" : ""), textArea, Color.White, 0.85f); + + if (field.Text.Length == 0) + return; + + var clear = new Rectangle(bounds.Right - 26, bounds.Y + (bounds.Height - 20) / 2, 20, 20); + bool clearHot = Hot(clear); + Icon(GameTextures.Request("Images/UI/SearchCancel"), clear); + if (clearHot) + Render.Outline(SpriteBatch, clear, Yellow * 0.6f); + if (clearHot && Click) + { + field.Text = ""; + + Sound(12); + } + } + + public static float ValueBar(string id, Rectangle row, float value, float minimum, float maximum, float fallback, Utils.ColorLerpMethod? gradient = null) + { + const float BarScale = 0.8f; + bool hot = Hot(row); + float barWidth = (GameTextures.Get("ColorBar")?.Width ?? 177) * BarScale; + IngameOptions.valuePosition = new Vector2(row.Right, row.Y + row.Height / 2f); + bool active = ActiveSlider == id; + int lockState = active ? 1 : !hot || ActiveSlider is not null ? 2 : 0; + float fraction = IngameOptions.DrawValueBar(SpriteBatch, BarScale, (value - minimum) / (maximum - minimum), lockState, gradient ?? Utils.ColorLerp_BlackToWhite); + + var bar = new Rectangle((int)(row.Right - barWidth), row.Y, (int)barWidth, row.Height); + if (Hot(bar) && Click && ActiveSlider is null) + { + ActiveSlider = id; + active = true; + } + + if (active && Down) + value = MathHelper.Clamp(minimum + fraction * (maximum - minimum), minimum, maximum); + + if (hot && RightClick) + { + value = fallback; + + Sound(12); + } + + return value; + } +} diff --git a/src/TerraSharp/UI/Window.cs b/src/TerraSharp/UI/Window.cs new file mode 100644 index 0000000..8be7007 --- /dev/null +++ b/src/TerraSharp/UI/Window.cs @@ -0,0 +1,28 @@ +using Microsoft.Xna.Framework; + +namespace TerraSharp.UI; + +public abstract class Window(string id, string title, Vector2 position, int width, int height) +{ + public string Id { get; } = id; + + public string Title { get; } = title; + + public Vector2 Pos { get; set; } = position; + + public int Width { get; } = width; + + public int Height { get; } = height; + + public bool Open { get; set; } + + public Rectangle Bounds => new((int)Pos.X, (int)Pos.Y, Width, Height); + + public abstract void DrawBody(Rectangle body); + + public virtual bool DragHandle(Rectangle body, Point mouse) => false; + + public virtual void OnOpen() + { + } +} diff --git a/src/TerraSharp/UI/WindowManager.cs b/src/TerraSharp/UI/WindowManager.cs new file mode 100644 index 0000000..79ed624 --- /dev/null +++ b/src/TerraSharp/UI/WindowManager.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Terraria; +using Terraria.GameInput; +using TerraSharp.Core; + +namespace TerraSharp.UI; + +public static class WindowManager +{ + public static readonly MainWindow Menu = new(); + public static readonly ItemBrowser Items = new(); + public static readonly BuffBrowser Buffs = new(); + + const int TitleHeight = 40; + const int MaxInputLength = 40; + + static readonly List Stack = [Items, Buffs, Menu]; + static Window? _dragging; + static Vector2 _dragOffset; + + public static TextField? Focus { get; set; } + + public static bool MouseOver { get; private set; } + + public static bool AnyOpen => Stack.Exists(w => w.Open); + + public static IEnumerable All => Stack; + + public static bool Contains(Point point) => Stack.Exists(w => w.Open && w.Bounds.Contains(point)); + + public static void Show(Window window) + { + if (!window.Open) + { + window.Open = true; + window.OnOpen(); + Widgets.Sound(10); + } + + BringToFront(window); + } + + public static void Hide(Window window) + { + if (!window.Open) + return; + + window.Open = false; + Widgets.Sound(11); + Unfocus(); + } + + public static void Toggle(Window window) + { + if (window.Open) + Hide(window); + else + Show(window); + } + + public static void HideAll() + { + foreach (Window window in Stack) + window.Open = false; + Unfocus(); + } + + static void BringToFront(Window window) + { + Stack.Remove(window); + Stack.Add(window); + } + + public static void Unfocus() + { + if (Focus is null && Hotkeys.Capturing is null) + return; + + Focus = null; + Hotkeys.Capturing = null; + Main.blockInput = false; + Main.clrInput(); + } + + public static void ConsumeInput(Player player) + { + if (MouseOver) + { + player.mouseInterface = true; + PlayerInput.ScrollWheelDelta = 0; + } + + if (Hotkeys.Capturing is not null) + Main.blockInput = true; + if (Focus is null) + return; + + Main.blockInput = true; + PlayerInput.WritingText = true; + Main.instance.HandleIME(); + string text = Main.GetInputText(Focus.Text, false); + Focus.Text = text.Length > MaxInputLength ? text.Substring(0, MaxInputLength) : text; + if (Main.inputTextEnter || Main.inputTextEscape) + Unfocus(); + } + + static Vector2 ClampPosition(Window window, Vector2 position) => new(MathHelper.Clamp(position.X, 0f, Math.Max(0f, Main.screenWidth / Main.UIScale - window.Width)), MathHelper.Clamp(position.Y, 0f, Math.Max(0f, Main.screenHeight / Main.UIScale - window.Height))); + + public static void Draw(SpriteBatch spriteBatch) + { + Widgets.SpriteBatch = spriteBatch; + Widgets.Poll(); + + foreach (Window window in Stack) + window.Pos = ClampPosition(window, window.Pos); + + Window? top = Stack.FindLast(w => w.Open && w.Bounds.Contains(Widgets.Mouse)); + MouseOver = Widgets.InputOk && top is not null; + if (Widgets.Click) + if (top is null) + Unfocus(); + else + BringToFront(top); + + if (!Widgets.Down) + _dragging = null; + if (_dragging is not null) + { + Vector2 position = new Vector2(Widgets.Mouse.X, Widgets.Mouse.Y) + _dragOffset; + _dragging.Pos = ClampPosition(_dragging, position); + } + + foreach (Window window in Stack.ToArray()) + { + if (!window.Open) + continue; + + Widgets.Blocked = window != top || _dragging is not null; + DrawFrame(window); + } + + Widgets.Blocked = false; + + if (Widgets.Tooltip is not null && MouseOver) + Main.instance.MouseText(Widgets.Tooltip, 0, 0, -1, -1, -1, -1, 0); + } + + static void DrawFrame(Window window) + { + Rectangle bounds = window.Bounds; + Render.Fill(Widgets.SpriteBatch, new Rectangle(bounds.X + 4, bounds.Y + 4, bounds.Width - 8, bounds.Height - 8), new Color(16, 20, 40) * 0.92f); + Widgets.Panel(bounds, Widgets.WindowBg); + + bool hasTitle = window.Title.Length > 0; + int bodyTop = hasTitle ? 50 : 12; + var body = new Rectangle(bounds.X + 12, bounds.Y + bodyTop, bounds.Width - 24, bounds.Height - bodyTop - 12); + var title = new Rectangle(bounds.X, bounds.Y, bounds.Width, TitleHeight); + if (hasTitle) + { + Render.TextMid(Widgets.SpriteBatch, window.Title, new Rectangle(bounds.X + 16, bounds.Y + 2, bounds.Width, TitleHeight), Color.White, 1.0f); + Widgets.Separator(bounds.X + 10, bounds.Y + TitleHeight, bounds.Width - 20); + } + + bool closeHot = false; + if (hasTitle) + { + var close = new Rectangle(bounds.Right - 40, bounds.Y + 7, 28, 28); + closeHot = Widgets.Hot(close); + if (closeHot) + { + Widgets.Panel(close, Widgets.ButtonHot, Widgets.Yellow); + Widgets.Tooltip = "Close"; + } + Widgets.Icon(GameTextures.Request("Images/UI/SearchCancel"), new Rectangle(close.X + 4, close.Y + 4, 20, 20), null, 2f, closeHot ? Widgets.Yellow : Color.White); + if (closeHot && Widgets.Click) + { + Hide(window); + return; + } + } + + bool dragHot = !closeHot && (hasTitle ? Widgets.Hot(title) : Widgets.Hot(bounds) && (!body.Contains(Widgets.Mouse) || window.DragHandle(body, Widgets.Mouse))); + if (dragHot) + Widgets.Tooltip = "Drag to move"; + if (dragHot && Widgets.Click) + { + _dragging = window; + _dragOffset = window.Pos - new Vector2(Widgets.Mouse.X, Widgets.Mouse.Y); + } + + window.DrawBody(body); + } +} diff --git a/src/TerraSharp/Visuals/AimOverlay.cs b/src/TerraSharp/Visuals/AimOverlay.cs new file mode 100644 index 0000000..c9afcf1 --- /dev/null +++ b/src/TerraSharp/Visuals/AimOverlay.cs @@ -0,0 +1,81 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Terraria; +using TerraSharp.Core; +using TerraSharp.Features.Combat; +using TerraSharp.UI; + +namespace TerraSharp.Visuals; + +public static class AimOverlay +{ + static readonly Color Unverified = new(255, 150, 40); + + public static void Draw(SpriteBatch spriteBatch) + { + DrawPreview(spriteBatch); + if (!Settings.AimDrawTarget || !(Settings.Aimbot || Settings.AutoAttack)) + return; + + if (Settings.Aimbot && Settings.AimFov > 0f) + DrawRadius(spriteBatch); + if (Aimbot.Target is { active: true } target) + DrawTarget(spriteBatch, target); + else if (Aimbot.Unreachable is { active: true } unreachable) + DrawUnreachable(spriteBatch, unreachable); + } + + static void DrawPreview(SpriteBatch spriteBatch) + { + if (ShotPreview.Paths.Count == 0) + return; + + using var scope = Profiler.Measure(Feature.ShotPreview); + foreach (ShotPreview.Path path in ShotPreview.Paths) + { + for (int i = 1; i < path.Count; i++) + Render.Line(spriteBatch, Render.ToScreen(path.Points[i - 1]), Render.ToScreen(path.Points[i]), Color.White * (0.55f * (1f - i / (float)path.Count * 0.7f))); + + Vector2 end = Render.ToScreen(path.Points[path.Count - 1]); + Render.Fill(spriteBatch, new Rectangle((int)end.X - 2, (int)end.Y - 2, 5, 5), Color.White * 0.8f); + } + } + + // Main.mouseX in this unscaled layer is in backbuffer pixels. + static void DrawRadius(SpriteBatch spriteBatch) + { + const int Segments = 48; + var center = new Vector2(Main.mouseX, Main.mouseY); + Color color = (Aimbot.Firing ? Widgets.Yellow : Color.White) * 0.25f; + Vector2 Point(int segment) => center + new Vector2((float)Math.Cos(MathHelper.TwoPi * segment / Segments), (float)Math.Sin(MathHelper.TwoPi * segment / Segments)) * Settings.AimFov; + for (int i = 0; i < Segments; i++) + Render.Line(spriteBatch, Point(i), Point(i + 1), color); + } + + static void DrawTarget(SpriteBatch spriteBatch, NPC target) + { + Vector2 aim = Render.ToScreen(Aimbot.AimPoint); + if (!Aimbot.Firing) + { + Render.Corners(spriteBatch, Render.ToScreen(target.position) - new Vector2(5), Render.ToScreen(target.position + target.Size) + new Vector2(5), Color.White * 0.8f, 1.5f); + Render.Fill(spriteBatch, new Rectangle((int)aim.X - 2, (int)aim.Y - 2, 4, 4), Color.White * 0.8f); + return; + } + + float pulse = 6f + 3f * (float)Math.Sin(DateTime.Now.TimeOfDay.TotalSeconds * 12.0); + Color color = Aimbot.Verified || !Settings.AimExact ? Widgets.Yellow : Unverified; + Render.Line(spriteBatch, Render.ToScreen(Aimbot.Origin), aim, color * 0.45f); + Render.Corners(spriteBatch, Render.ToScreen(target.position) - new Vector2(pulse), Render.ToScreen(target.position + target.Size) + new Vector2(pulse), color, 2.5f); + Render.Fill(spriteBatch, new Rectangle((int)aim.X - 3, (int)aim.Y - 3, 7, 7), Widgets.Yellow); + } + + static void DrawUnreachable(SpriteBatch spriteBatch, NPC npc) + { + Vector2 topLeft = Render.ToScreen(npc.position) - new Vector2(5), bottomRight = Render.ToScreen(npc.position + npc.Size) + new Vector2(5); + Render.Corners(spriteBatch, topLeft, bottomRight, Color.Gray * 0.8f, 1f); + Vector2 center = (topLeft + bottomRight) / 2f; + Render.Line(spriteBatch, center - new Vector2(5), center + new Vector2(5), Color.Gray, 2f); + Render.Line(spriteBatch, center + new Vector2(-5, 5), center + new Vector2(5, -5), Color.Gray, 2f); + } +} diff --git a/src/TerraSharp/Visuals/Esp.cs b/src/TerraSharp/Visuals/Esp.cs new file mode 100644 index 0000000..cb0a5e8 --- /dev/null +++ b/src/TerraSharp/Visuals/Esp.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Terraria; +using TerraSharp.Core; +using TerraSharp.Features; +using TerraSharp.Features.Combat; +using TerraSharp.Simulation; +using TerraSharp.UI; + +namespace TerraSharp.Visuals; + +public static class Esp +{ + static Vector2 _origin; + static float _maxDistanceSq; + static Player _player = null!; + static readonly Vector2[] Path = new Vector2[181]; + static readonly List<(Vector2 Position, float Scale, int Life, int Max)> HealthBars = []; + + // Stored colors carry opacity in A (straight alpha); the batch expects premultiplied. + static Color Premultiplied(Color color) => new Color(color.R, color.G, color.B) * (color.A / 255f); + + public static void Draw(SpriteBatch spriteBatch) + { + using var scope = Profiler.Measure(Feature.Esp); + _player = Main.LocalPlayer; + _origin = Render.ToScreen(_player.Center); + _maxDistanceSq = Settings.EspMaxDistance > 0 ? Settings.EspMaxDistance * 16f * Settings.EspMaxDistance * 16f : float.MaxValue; + HealthBars.Clear(); + + if (Settings.EspProjHostile || Settings.EspProjFriendly) + DrawProjectiles(spriteBatch); + DrawPlayers(spriteBatch); + DrawNpcs(spriteBatch); + if (Settings.EspItems) + DrawItems(spriteBatch); + if (Settings.EspWaypoints) + foreach (Waypoint waypoint in Waypoints.Current) + Box(spriteBatch, waypoint.Position, _player.width, _player.height, Premultiplied(Settings.ColWaypoint), waypoint.Name, -1, -1, true, true); + } + + static void DrawProjectiles(SpriteBatch spriteBatch) + { + for (int i = 0; i < Main.maxProjectiles; i++) + { + Projectile projectile = Main.projectile[i]; + if (!projectile.active || projectile.bobber) + continue; + + bool hostile = projectile.hostile; + if (hostile ? !Settings.EspProjHostile : !(Settings.EspProjFriendly && projectile.friendly)) + continue; + + Color color = Premultiplied(hostile ? Settings.ColProjHostile : Settings.ColProjFriendly); + if (Box(spriteBatch, projectile.position, projectile.width, projectile.height, color, hostile ? projectile.Name : null, -1, -1, false, false) && Settings.EspProjPath) + ProjectilePath(spriteBatch, i, projectile, color); + } + } + + static void DrawPlayers(SpriteBatch spriteBatch) + { + for (int i = 0; i < Main.maxPlayers; i++) + { + Player player = Main.player[i]; + if (player is not { active: true, dead: false }) + continue; + + bool local = i == Main.myPlayer; + if (local ? Settings.EspLocal : Settings.EspPlayers) + Box(spriteBatch, player.position, player.width, player.height, Premultiplied(local ? Settings.ColLocal : Settings.ColPlayers), player.name, player.statLife, player.statLifeMax2, !local, true, PlayerBar(player)); + } + } + + static void DrawNpcs(SpriteBatch spriteBatch) + { + for (int i = 0; i < Main.maxNPCs; i++) + { + NPC npc = Main.npc[i]; + if (!npc.active || NpcColor(npc) is not { } color) + continue; + + Box(spriteBatch, npc.position, npc.width, npc.height, Premultiplied(color), npc.GivenOrTypeName, npc.life, npc.lifeMax, true, true, NpcBar(npc)); + } + } + + static Color? NpcColor(NPC npc) + { + if (Hostility.IsCritter(npc)) + return Settings.EspCritters ? Settings.ColCritters : null; + if (npc.townNPC || npc.friendly) + return Settings.EspTown ? Settings.ColTown : null; + if (!Settings.EspHostile) + return null; + + return npc.boss ? (Settings.EspBossRainbow ? Rainbow() : Settings.ColBoss) : Settings.ColHostile; + } + + static void DrawItems(SpriteBatch spriteBatch) + { + for (int i = 0; i < Main.maxItems; i++) + { +#if TMODLOADER + Item drop = Main.item[i], inner = drop; + if (!drop.active || drop.type == 0) + continue; +#else + WorldItem drop = Main.item[i]; + if (drop is not { active: true, inner: { type: not 0 } inner }) + continue; +#endif + + string label = inner.stack > 1 ? $"{inner.Name} x{inner.stack}" : inner.Name; + Box(spriteBatch, drop.position, drop.width, drop.height, Premultiplied(Settings.ColItems), label, -1, -1, true, true); + } + } + + // Anchors and scale match Main.DrawInterface_14_EntityHealthBars. + static (Vector2 Position, float Scale) NpcBar(NPC npc) + { + Vector2 position = npc.position + npc.netOffset; + float y = Main.HealthBarDrawSettings == 2 ? position.Y - 24f - Main.NPCAddHeight(npc) / 2f : position.Y + npc.height + 10f + Main.NPCAddHeight(npc); + float scale = npc.type is 4 or 35 or 36 or 50 or 87 or 113 or 114 or (>= 125 and <= 131) or (>= 134 and <= 136) or 222 or (>= 245 and <= 249) or 262 or 266 or 370 or 439 or 454 or 510 or 551 or 564 or 565 or 576 or 577 or 621 ? 1.5f : 1f; + return (new Vector2(position.X + npc.width / 2f, y + npc.gfxOffY), scale); + } + + static (Vector2 Position, float Scale) PlayerBar(Player player) + { +#if TMODLOADER + Vector2 bottom = player.Bottom; +#else + Vector2 bottom = player.Bottom + player.netOffset; +#endif + return (new Vector2(bottom.X, bottom.Y + player.gfxOffY + (Main.HealthBarDrawSettings == 2 ? -20f : 10f)), 1f); + } + + public static void DrawHealthBars() + { + using var scope = Profiler.Measure(Feature.Esp); + foreach ((Vector2 position, float scale, int life, int maximum) in HealthBars) + Main.instance.DrawHealthBar(position.X, position.Y, life, maximum, 1f, scale, false); + } + + static void ProjectilePath(SpriteBatch spriteBatch, int slot, Projectile projectile, Color color) + { + if (Sandbox.Held(projectile)) + return; + + Vector2[] path = Path; + int from = 0; + int count = Sandbox.LivePath(projectile, slot, Clock.Tick, out Vector2[] live, out int liveFrom); + if (count > 0) + { + (path, from) = (live, liveFrom); + } + else + { + FlightModel model = Ballistics.Get(projectile.type); + if (!Ballistics.Current(slot, out Vector2 velocity, out int age)) + (velocity, age) = (projectile.velocity * (1 + projectile.extraUpdates), 0); + if (velocity.LengthSquared() < 0.01f) + return; + + int ticks = Math.Min(180, Math.Max(1, projectile.timeLeft / (1 + projectile.extraUpdates))); + if (model.Life > 0) + ticks = Math.Min(ticks, Math.Max(1, model.Life - age)); + count = Ballistics.Path(model, projectile.Center, velocity, age, ticks, Path); + } + + for (int i = from + 1; i < count; i++) + { + float fade = 1f - (i - from) / (float)Math.Max(1, count - from) * 0.8f; + Render.Line(spriteBatch, Render.ToScreen(path[i - 1]), Render.ToScreen(path[i]), color * (0.75f * fade), 1.5f); + } + + if (count > from + 1) + { + Vector2 end = Render.ToScreen(path[count - 1]); + Render.Fill(spriteBatch, new Rectangle((int)end.X - 2, (int)end.Y - 2, 4, 4), color); + } + } + + public static Color Rainbow() => Main.hslToRgb((float)(DateTime.Now.TimeOfDay.TotalSeconds * 0.5 % 1.0), 1f, 0.55f, 255); + + static bool Box(SpriteBatch spriteBatch, Vector2 position, int width, int height, Color color, string? label, int life, int lifeMax, bool tracer, bool details, (Vector2 Position, float TextScale)? healthBar = null) + { + Vector2 center = position + new Vector2(width / 2f, height / 2f); + float distanceSq = Vector2.DistanceSquared(center, _player.Center); + if (distanceSq > _maxDistanceSq) + return false; + + Vector2 topLeft = Render.ToScreen(position), bottomRight = Render.ToScreen(position + new Vector2(width, height)); + bool onScreen = !(bottomRight.X < 0 || bottomRight.Y < 0 || topLeft.X > Main.screenWidth || topLeft.Y > Main.screenHeight); + if (Settings.EspLines && tracer) + Render.Line(spriteBatch, _origin, onScreen ? new Vector2((topLeft.X + bottomRight.X) / 2f, bottomRight.Y) : (topLeft + bottomRight) / 2f, color * 0.6f); + if (!onScreen) + return false; + + int thickness = Math.Max(1, (int)Settings.EspThickness); + var bounds = new Rectangle((int)topLeft.X, (int)topLeft.Y, Math.Max(1, (int)(bottomRight.X - topLeft.X)), Math.Max(1, (int)(bottomRight.Y - topLeft.Y))); + switch (Settings.EspStyle) + { + case EspBoxStyle.Corners: + Render.Corners(spriteBatch, topLeft, bottomRight, color, Settings.EspThickness); + break; + case EspBoxStyle.Filled: + Render.Fill(spriteBatch, bounds, color * 0.25f); + Render.Outline(spriteBatch, bounds, color, thickness); + break; + default: + Render.Outline(spriteBatch, bounds, color, thickness); + break; + } + + if (!details) + return true; + + const float TextScale = 0.7f; + float lineHeight = Render.LineHeight(TextScale); + float above = bounds.Y - lineHeight - 2; + if (Settings.EspNames && !string.IsNullOrEmpty(label)) + { + Render.TextCenteredAt(spriteBatch, label!, bounds.Center.X, above, color, TextScale); + above -= lineHeight; + } + if (Settings.EspHealthText && lifeMax > 0) + Render.TextCenteredAt(spriteBatch, $"{life} / {lifeMax}", bounds.Center.X, above, Color.Lerp(new Color(255, 90, 90), new Color(120, 235, 120), MathHelper.Clamp(life / (float)lifeMax, 0f, 1f)), TextScale); + + float below = bounds.Bottom + 4; + if (Settings.EspHealth && healthBar is { } bar && lifeMax > 0 && life > 0) + { + HealthBars.Add((bar.Position, bar.TextScale, life, lifeMax)); + if (Main.HealthBarDrawSettings != 2) + below = Math.Max(below, Render.ToScreen(bar.Position).Y + 14f * bar.TextScale * Main.GameViewMatrix.Zoom.Y); + } + + // Terraria's compass and depth meter use 2 feet per tile. + if (Settings.EspDistance) + Render.TextCenteredAt(spriteBatch, $"{(int)(Math.Sqrt(distanceSq) / 8f)} ft", bounds.Center.X, below, color * 0.9f, TextScale); + + return true; + } +}