├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── change-a-command.md │ ├── code-quality.md │ ├── documentation.md │ └── new-command.md └── workflows │ └── release.yml ├── .gitignore ├── .gitmodules ├── .markdownlint.json ├── BUILDING.MD ├── CHANGELOG.md ├── Code ├── AutoCompletion │ ├── AutoCompleteAttribute.cs │ ├── AutoCompleteManager.cs │ ├── AutoCompleteParser.cs │ └── README.md ├── DT-Commands │ ├── Buffs.cs │ ├── Command_Noclip.cs │ ├── Command_Teleport.cs │ ├── CurrentRun.cs │ ├── DEBUG.cs │ ├── Items.cs │ ├── Lists.cs │ ├── LobbyManagement.cs │ ├── Macros.cs │ ├── Miscellaneous.cs │ ├── Money.cs │ ├── PlayerCommands.cs │ ├── Profile.cs │ └── Spawners.cs ├── DebugToolkit.cs ├── Hooks.cs ├── IgnoreAccessModifiers.cs ├── Lang.cs ├── Log.cs ├── MacroSystem.cs ├── NetworkManager.cs ├── PermissionSystem.cs ├── StringFinder.cs └── Util.cs ├── DebugToolkit.csproj ├── DebugToolkit.sln ├── FodyWeavers.xml ├── FodyWeavers.xsd ├── LICENSE ├── NetworkWeaver ├── Unity.Cecil.Mdb.dll ├── Unity.Cecil.Pdb.dll ├── Unity.Cecil.dll └── Unity.UNetWeaver.exe ├── README.md ├── Resources ├── GlobalSuppressions.cs └── icon.ico ├── Thunderstore ├── icon.png ├── icon.xcf └── thunderstore.toml ├── libs ├── MMHOOK_RoR2.dll ├── R2API-LICENSE.txt ├── R2API.dll ├── ShareSuite-LICENSE.txt └── ShareSuite.dll └── nuget.config /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # IDE0011: Add braces 4 | csharp_prefer_braces = when_multiline:suggestion 5 | 6 | # IDE0035: Unreachable code detected 7 | dotnet_diagnostic.IDE0035.severity = suggestion 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Does the bug happen without input** 14 | Yes/no 15 | 16 | **If input is required, which scenes does it go wrong in** 17 | Main menu/pregame lobby/ingame/all of them 18 | 19 | **Do you have other mods installed?** 20 | A modlist would be most helpful for spotting conflicts. You do not have to mention R2API and bepinex. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/change-a-command.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Change a command 3 | about: Is an existing command not working as expected or does it need to do more? 4 | title: 'Change Command: ' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Which Command** 11 | `command` 12 | 13 | **What should it do?** 14 | Describe the extended new behavior. Be as exhaustive as you can. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/code-quality.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Code Quality 3 | about: Found something bad in the code? 4 | title: 'Code Quality: ' 5 | labels: documentation, Code Smell 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please link files and line numbers. 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Missing something in readmes or command output? Report it here 4 | title: "Documentation: " 5 | labels: Documenation 6 | assignees: '' 7 | 8 | --- 9 | 10 | Where is this documentation lacking: 11 | Readme/Command output 12 | 13 | If it's a command, describe the command input producing the wrong documentation. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-command.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New command 3 | about: Suggest a new command 4 | title: 'New Command: <NAME>' 5 | labels: Feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe your new command** 11 | Give a concrete description on when the command should work, and what it should do. 12 | 13 | **Use cases** 14 | Give an example of when someone might want to use this. 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Thunderstore Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: windows-latest 11 | steps: 12 | 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Setup NuGet 17 | uses: NuGet/setup-nuget@v1 18 | 19 | - name: Setup TCLI 20 | run: dotnet tool install --global tcli 21 | 22 | - name: Setup dotnet 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: 7.0.x 26 | 27 | - name: Build and Publish 28 | run: | 29 | dotnet build --configuration Release 30 | 31 | cd Thunderstore 32 | tcli publish --token ${{ secrets.TCLI_AUTH_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | RoR2Cheats.dll 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # JustCode is a .NET coding add-in 131 | .JustCode 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | #Manual 353 | DebugToolkit.dll 354 | DebugToolkit.zip 355 | CurrentCommit 356 | 357 | NetworkWeaver/* 358 | !NetworkWeaver/Unity.Cecil.dll 359 | !NetworkWeaver/Unity.Cecil.Mdb.dll 360 | !NetworkWeaver/Unity.Cecil.Pdb.dll 361 | !NetworkWeaver/Unity.UNetWeaver.exe 362 | 363 | *.prepatch 364 | 365 | Thunderstore/contents/README.md 366 | Thunderstore/contents/*.zip -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD003": { "style": "atx_closed" }, 4 | "MD007": { "indent": 4 }, 5 | "MD013": false, 6 | "MD041": false, 7 | "no-hard-tabs": false 8 | } -------------------------------------------------------------------------------- /BUILDING.MD: -------------------------------------------------------------------------------- 1 | # Building 2 | 3 | So you want to build your own DebugToolkit? 4 | 5 | Here's how to get started: 6 | 7 | 1. Acquire a copy of the project: 8 | * Fork the project, and clone it from there. (Recommended for Contributing) 9 | * Clone the project directly (Recommended for Primary Contributors) 10 | * Download the zip file (Recommended for people without Git) 11 | 2. Setup the libraries 12 | * Run the setup.bat in the libs folder. This file requires Git. 13 | * You may also acquire the dependencies manually from your local risk of rain 2 installation and bepinex folder. 14 | * If that also isn't an option, the setup.bat pulls from the ror2libs github repository, where you can find a powershell script to download it. 15 | 3. *[OPTIONAL]* Download the [NetworkWeaver files](https://cdn.discordapp.com/attachments/697919673664274563/697919946143039588/NetworkWeaver.zip). Extract this such that the `/NetworkWeaver` sits next to `/Resources` and all other folders. 16 | * These are needed if you want to build networking. 17 | 4. Open the project in Visual Studio. 18 | * Additionally, you may want to select your configuration: 19 | * Release: For what you will see on Thunderstore. 20 | * BLEEDING-EDGE: The same as Release, save that it contains a warning that the build is not final. 21 | * Debug: Contains additional commands for debugging other commands. 22 | * NO-UNET: Skips the network weaving post-build step. 23 | 5. Press build! The file should appear in the main project directory. 24 | -------------------------------------------------------------------------------- /Code/AutoCompletion/AutoCompleteAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace DebugToolkit 7 | { 8 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 9 | public sealed class AutoCompleteAttribute : Attribute 10 | { 11 | public string args; 12 | 13 | public AutoCompleteAttribute(string args) 14 | { 15 | this.args = args; 16 | } 17 | 18 | internal Result Parse(AutoCompleteParser parser) 19 | { 20 | var matches = Regex.Matches(args, @"\{(.*?)\}|\[(.*?)\]|<(.*?)>"); 21 | var parameters = new List<string>[matches.Count]; 22 | for (int i = 0; i < matches.Count; i++) 23 | { 24 | var groups = matches[i].Groups; 25 | var match = (!string.IsNullOrEmpty(groups[1].Value) ? groups[1] 26 | : !string.IsNullOrEmpty(groups[2].Value) ? groups[2] : groups[3] 27 | ).Value.Split(':')[0]; 28 | var options = Regex.Match(match, @"\((.*)\)"); 29 | parameters[i] = new List<string>(); 30 | if (options.Success) 31 | { 32 | var names = options.Groups[1].Value.Split('|').Select(s => s.Trim()); 33 | foreach (var name in names) 34 | { 35 | var trimmed = name.Trim(); 36 | if (trimmed.StartsWith("'") && trimmed.EndsWith("'")) 37 | { 38 | parameters[i].Add(trimmed); 39 | } 40 | else 41 | { 42 | if (parser.TryGetStaticVariable(trimmed, out _) || parser.TryGetDynamicVariable(trimmed, out _)) 43 | { 44 | parameters[i].Add(trimmed); 45 | } 46 | } 47 | } 48 | } 49 | else 50 | { 51 | match = match.Trim(); 52 | if (parser.TryGetStaticVariable(match, out _) || parser.TryGetDynamicVariable(match, out _)) 53 | { 54 | parameters[i].Add(match); 55 | } 56 | } 57 | } 58 | return new Result(parser, string.Join(" ", matches.Cast<Match>().Select(m => m.Groups[0].Value)), parameters); 59 | } 60 | 61 | internal class Result 62 | { 63 | internal AutoCompleteParser parser; 64 | internal string signature; 65 | internal List<string>[] parameters; 66 | 67 | internal Result(AutoCompleteParser parser, string signature, List<string>[] parameters) 68 | { 69 | this.parser = parser; 70 | this.signature = signature; 71 | this.parameters = parameters; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Code/AutoCompletion/AutoCompleteManager.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace DebugToolkit 7 | { 8 | internal static class AutoCompleteManager 9 | { 10 | private static readonly Dictionary<string, AutoCompleteAttribute.Result> commands = new Dictionary<string, AutoCompleteAttribute.Result>(); 11 | 12 | internal static string CurrentCommand { get; private set; } 13 | internal static List<string>[] CurrentParameters { get; private set; } 14 | internal static string CurrentSignature { get; private set; } 15 | 16 | internal static void PrepareCommandOptions(string commandName) 17 | { 18 | commandName = commandName.ToLower(); 19 | if (CurrentCommand == commandName) 20 | { 21 | return; 22 | } 23 | CurrentCommand = commandName; 24 | if (!commands.TryGetValue(commandName, out var data)) 25 | { 26 | CurrentParameters = new List<string>[0]; 27 | CurrentSignature = null; 28 | return; 29 | } 30 | var parser = data.parser; 31 | var options = new List<string>[data.parameters.Length]; 32 | for (int i = 0; i < options.Length; i++) 33 | { 34 | options[i] = new List<string>(); 35 | foreach (var name in data.parameters[i]) 36 | { 37 | if (name.StartsWith("'") && name.EndsWith("'")) 38 | { 39 | options[i].Add(name.Trim('\'')); 40 | } 41 | else if (parser.TryGetStaticVariable(name, out var strings)) 42 | { 43 | options[i].AddRange(strings); 44 | } 45 | else if (parser.TryGetDynamicVariable(name, out var catalog)) 46 | { 47 | options[i].AddRange(catalog.Rebuild()); 48 | } 49 | } 50 | } 51 | CurrentParameters = options; 52 | CurrentSignature = data.signature; 53 | } 54 | 55 | internal static void ClearCommandOptions() 56 | { 57 | CurrentCommand = null; 58 | CurrentParameters = null; 59 | CurrentSignature = null; 60 | } 61 | 62 | internal static void RegisterCommand(string commandName, AutoCompleteAttribute.Result parameters) 63 | { 64 | commands[commandName.ToLower()] = parameters; 65 | } 66 | 67 | internal static void RegisterAutoCompleteCommands() 68 | { 69 | var parser = new AutoCompleteParser(); 70 | parser.RegisterStaticVariable("0", "0"); 71 | parser.RegisterStaticVariable("1", "1"); 72 | parser.RegisterStaticVariable("ai", MasterCatalog.allAiMasters.Select(i => $"{(int)i.masterIndex}|{i.name}|{StringFinder.GetLangInvar(StringFinder.GetMasterName(i))}")); 73 | parser.RegisterStaticVariable("artifact", ArtifactCatalog.artifactDefs.Select(i => $"{(int)i.artifactIndex}|{i.cachedName}|{StringFinder.GetLangInvar(i.nameToken)}")); 74 | parser.RegisterStaticVariable("body", BodyCatalog.allBodyPrefabBodyBodyComponents.Select(i => $"{(int)i.bodyIndex}|{i.name}|{StringFinder.GetLangInvar(i.baseNameToken)}")); 75 | parser.RegisterStaticVariable("buff", BuffCatalog.buffDefs.Select(i => $"{(int)i.buffIndex}|{StringFinder.GetLangInvar(i.name)}")); 76 | parser.RegisterStaticVariable("droptable", ItemTierCatalog.allItemTierDefs.OrderBy(i => i.tier).Select(i => $"{(int)i.tier}|{i.name}")); 77 | parser.RegisterStaticVariable("elite", new string[] { "-1|None" }. 78 | Concat(EliteCatalog.eliteDefs.Select(i => $"{(int)i.eliteIndex}|{i.name}|{StringFinder.GetLangInvar(i.modifierToken)}")) 79 | ); 80 | parser.RegisterStaticVariable("equip", EquipmentCatalog.equipmentDefs.Select(i => $"{(int)i.equipmentIndex}|{i.name}|{StringFinder.GetLangInvar(i.nameToken)}")); 81 | parser.RegisterStaticVariable("item", ItemCatalog.allItemDefs.Select(i => $"{(int)i.itemIndex}|{i.name}|{StringFinder.GetLangInvar(i.nameToken)}")); 82 | parser.RegisterStaticVariable("specific_stage", SceneCatalog.allSceneDefs.Where(i => !i.isOfflineScene).Select(i => $"{(int)i.sceneDefIndex}|{i.cachedName}|{StringFinder.GetLangInvar(i.nameToken)}")); 83 | 84 | parser.RegisterStaticVariable("dot", CollectEnumNames(typeof(DotController.DotIndex), typeof(sbyte)).Skip(1)); 85 | parser.RegisterStaticVariable("permission_level", CollectEnumNames(typeof(Permissions.Level), typeof(int))); 86 | parser.RegisterStaticVariable("team", CollectEnumNames(typeof(TeamIndex), typeof(sbyte))); 87 | 88 | parser.RegisterDynamicVariable("director_card", StringFinder.Instance.DirectorCards, "spawnCard"); 89 | parser.RegisterDynamicVariable("interactable", StringFinder.Instance.InteractableSpawnCards); 90 | parser.RegisterDynamicVariable("player", NetworkUser.instancesList, "userName"); 91 | 92 | parser.Scan(System.Reflection.Assembly.GetExecutingAssembly()); 93 | } 94 | 95 | private static IEnumerable<string> CollectEnumNames(Type enumType, Type castTo) 96 | { 97 | if (enumType == null) 98 | { 99 | Log.Message("Input type is null", Log.LogLevel.Warning, Log.Target.Bepinex); 100 | yield break; 101 | } 102 | if (!enumType.IsEnum) 103 | { 104 | Log.Message("Input type is not enum: " + enumType.Name, Log.LogLevel.Warning, Log.Target.Bepinex); 105 | yield break; 106 | } 107 | foreach (var field in enumType.GetFields()) 108 | { 109 | var name = field.Name; 110 | if (name != "value__" && name != "Count") 111 | { 112 | yield return $"{Convert.ChangeType(Enum.Parse(enumType, name), castTo)}|{name}"; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Code/AutoCompletion/AutoCompleteParser.cs: -------------------------------------------------------------------------------- 1 | using R2API.Utils; 2 | using RoR2; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace DebugToolkit 9 | { 10 | /// <summary> 11 | /// Register autocompletion options for ConCommands with the AutoCompleteAttribute. 12 | /// </summary> 13 | public sealed class AutoCompleteParser 14 | { 15 | private readonly Dictionary<string, string[]> staticVariables = new Dictionary<string, string[]>(); 16 | private readonly Dictionary<string, DynamicCatalog> dynamicVariables = new Dictionary<string, DynamicCatalog>(); 17 | 18 | public AutoCompleteParser() { } 19 | 20 | /// <summary> 21 | /// Register a variable name to represent a string. 22 | /// </summary> 23 | /// <param name="name">Name of the variable</param> 24 | /// <param name="value">The value it represents</param> 25 | public void RegisterStaticVariable(string name, string value) 26 | { 27 | RegisterStaticVariable(name, new string[] { value }); 28 | } 29 | 30 | /// <summary> 31 | /// Register a variable name to represent a static collection of strings. 32 | /// </summary> 33 | /// <param name="name">Name of the variable</param> 34 | /// <param name="values">Fixed-size iterable of the values</param> 35 | public void RegisterStaticVariable(string name, IEnumerable<string> values) 36 | { 37 | staticVariables[name] = values.ToArray(); 38 | } 39 | 40 | /// <summary> 41 | /// Register a variable name to represent a dynamic collection of strings. 42 | /// </summary> 43 | /// <param name="name">Name of the variable</param> 44 | /// <param name="catalog">Iterable that may change in size or values</param> 45 | /// <param name="nestedField">Concatenated strings with "/" that represent the field to select (optional)</param> 46 | /// <param name="isToken">Whether the selected field is a language token (optional)</param> 47 | /// <param name="showIndex">Whether the final string will include its positional index in the collection (optional)</param> 48 | public void RegisterDynamicVariable(string name, IEnumerable<object> catalog, string nestedField = "", bool isToken = false, bool showIndex = true) 49 | { 50 | dynamicVariables[name] = new DynamicCatalog(catalog, nestedField, isToken, showIndex); 51 | } 52 | 53 | internal bool TryGetStaticVariable(string name, out string[] strings) 54 | { 55 | return staticVariables.TryGetValue(name, out strings); 56 | } 57 | 58 | internal bool TryGetDynamicVariable(string name, out DynamicCatalog catalog) 59 | { 60 | return dynamicVariables.TryGetValue(name, out catalog); 61 | } 62 | 63 | /// <summary> 64 | /// Scan an assembly for commands with autocompletion options. 65 | /// </summary> 66 | /// <param name="assembly">The assembly to scan</param> 67 | public void Scan(Assembly assembly) 68 | { 69 | if (assembly == null) 70 | { 71 | Log.Message($"Null assembly encountered for autocompletion scanning", Log.LogLevel.Warning, Log.Target.Bepinex); 72 | return; 73 | } 74 | Type[] types; 75 | try 76 | { 77 | types = assembly.GetTypes(); 78 | } 79 | catch (ReflectionTypeLoadException ex) 80 | { 81 | types = ex.Types.Where(x => x != null).ToArray(); 82 | foreach (var e in ex.LoaderExceptions) 83 | { 84 | Log.Message(ex.Message, Log.LogLevel.Error, Log.Target.Bepinex); 85 | } 86 | } 87 | foreach (var methodInfo in types.SelectMany(x => x.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))) 88 | { 89 | object[] attributes; 90 | try 91 | { 92 | attributes = methodInfo.GetCustomAttributes(false); 93 | } 94 | catch (TypeLoadException ex) 95 | { 96 | Log.Message(ex.Message, Log.LogLevel.Error, Log.Target.Bepinex); 97 | continue; 98 | } 99 | catch (InvalidOperationException ex) 100 | { 101 | Log.Message(ex.Message, Log.LogLevel.Error, Log.Target.Bepinex); 102 | continue; 103 | } 104 | if (attributes == null) 105 | { 106 | continue; 107 | } 108 | var autoCompletionAttribute = attributes.OfType<AutoCompleteAttribute>().DefaultIfEmpty(null).FirstOrDefault(); 109 | if (autoCompletionAttribute != null) 110 | { 111 | var conCommandAttributes = attributes.OfType<ConCommandAttribute>().ToArray(); 112 | if (conCommandAttributes.Length > 0) 113 | { 114 | var p = autoCompletionAttribute.Parse(this); 115 | foreach (var conCommand in conCommandAttributes) 116 | { 117 | AutoCompleteManager.RegisterCommand(conCommand.commandName, p); 118 | } 119 | } 120 | } 121 | } 122 | } 123 | 124 | internal class DynamicCatalog 125 | { 126 | private readonly IEnumerable<object> catalog; 127 | private readonly string nestedField; 128 | private readonly bool isToken; 129 | private readonly bool showIndex; 130 | 131 | internal DynamicCatalog(IEnumerable<object> catalog, string nestedField, bool isToken, bool showIndex) 132 | { 133 | this.catalog = catalog; 134 | this.nestedField = nestedField; 135 | this.isToken = isToken; 136 | this.showIndex = showIndex; 137 | } 138 | 139 | internal IEnumerable<string> Rebuild() 140 | { 141 | var block = !string.IsNullOrEmpty(nestedField) ? nestedField.Split('/') : new string[0]; 142 | var index = 0; 143 | foreach (object item in catalog) 144 | { 145 | string itemString; 146 | if (block.Length > 0) 147 | { 148 | var tmp = item.GetFieldValue<object>(block[0]); 149 | for (int i = 1; i < block.Length; i++) 150 | { 151 | tmp = tmp.GetFieldValue<object>(block[i]); 152 | } 153 | itemString = tmp.ToString(); 154 | } 155 | else 156 | { 157 | itemString = item.ToString(); 158 | } 159 | 160 | if (itemString.Contains("(RoR")) 161 | { 162 | itemString = itemString.Substring(0, itemString.IndexOf('(')); 163 | } 164 | 165 | itemString = isToken ? StringFinder.GetLangInvar(itemString) : StringFinder.RemoveSpacesAndAlike(itemString); 166 | yield return showIndex ? $"{index}|{itemString}" : itemString; 167 | 168 | index += 1; 169 | } 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /Code/AutoCompletion/README.md: -------------------------------------------------------------------------------- 1 | # Command Autocomplete 2 | 3 | ## About 4 | 5 | Command Autocomplete is an enhancement by DebugToolkit which provides autofill options for any argument in a command. 6 | 7 | It also features partial string highlighting and allows multiple strings as aliases for the same option. The match ordering will take into consideration the closest match for each option and whether the match is at the beginning of a string. 8 | 9 | QoL change: `Tab` and `LeftCtrl + Tab` can also be used similarly to the down/up arrows to cycle through the options. 10 | 11 | ## Usage 12 | 13 | Download the dll and include it in your project file 14 | 15 | ``` 16 | <ItemGroup> 17 | <Reference Include="DebugToolkit"> 18 | <HintPath>path\to\file\DebugToolkit.dll</HintPath> 19 | </Reference> 20 | </ItemGroup> 21 | ``` 22 | 23 | Pair `AutoCompleteAttribute` with any `ConCommandAttribute`s in your mod and then use `AutoCompleteParser` to register the autocompletion options. See [example](#example) at the bottom. 24 | 25 | Finally add a dependency in your manifest for `"IHarbHD-DebugToolkit-x.y.z"`. 26 | 27 | ### Attribute 28 | The string in the attribute is scanned for any tokens containing brackets, .e.g., `{}`, `[]`, and `<>`, which are used to define what options are available for each argument. The pattern within these brackets falls under two categories: 29 | - `variable`, where variable defines the collection of strings available. 30 | - `(variable|'string')`, where the options in the round brackets are separated by a pipe (`|`). Tokens surrounded by single quotes are taken as literal strings. 31 | 32 | Use the latter for explicit options or seemingly unrelated ones where a variable name can't provide an easy description. 33 | 34 | If a colon (`:`) appears in the argument definition, anything to its right will be ignored. This can be used to convey any additional information to the user, e.g., a default value or input type. 35 | 36 | Any of the following are valid examples (assume `x` represents the list of values "1", "2", "3"): 37 | - `{x}` - The options will be ["1", "2", "3"] 38 | - `[x:1]` - Same as above 39 | - `<argument_name (x|'something'):(int|string)>` - ["1", "2", "3", "something"] 40 | 41 | ### Variables 42 | 43 | A **static variable** is a shorthand for a fixed collection of strings, e.g., items, true/false, etc. 44 | 45 | **Dynamic variables** represent collections whose contents may change during runtime, e.g., number of players. These can be constructed as and when needed using reflection. Simply provide the collection object along with any futher nested field(s) if needed concatenated with `"/"`. For example `(list, "a/b/c")` is the same as accessing `list[i].a.b.c`. The optional argument `"isToken"` decides whether the resulting value will be matched against any language tokens. 46 | 47 | Each string in a variable can contain multiple values concatenated with `"|"`, which point to the same selection. This is useful for enum values, e.g., `"0|This"`, `"1|That"`, etc. For a dynamic variable use the optional argument `"showIndex"` to toggle this behavior. 48 | 49 | While scanning the assembly any unregistered variables encountered will be ignored. This is useful for inputs where autofill options would be counterproductive or unnecessary, e.g., all integers. 50 | 51 | ### Example 52 | 53 | ```cs 54 | using DebugToolkit; 55 | 56 | private void Awake() 57 | { 58 | RoR2Application.onLoad += delegate() 59 | { 60 | var parser = new AutoCompleteParser(); 61 | parser.RegisterStaticVariable("action", new string[] { "1", "2", "3" }); 62 | parser.RegisterDynamicVariable("player", NetworkUser.instancesList, "userName"); 63 | parser.Scan(System.Reflection.Assembly.GetExecutingAssembly()); 64 | }; 65 | } 66 | 67 | [ConCommand(...)] 68 | [AutoComplete("[action] [times:1] [(player|'everyone'|'self')]")] 69 | private static void CCDoSomething(ConCommandArgs args) { } 70 | ``` -------------------------------------------------------------------------------- /Code/DT-Commands/Command_Noclip.cs: -------------------------------------------------------------------------------- 1 | using KinematicCharacterController; 2 | using RoR2; 3 | using System; 4 | using UnityEngine; 5 | using UnityEngine.Networking; 6 | 7 | // ReSharper disable UnusedMember.Local 8 | // ReSharper disable InconsistentNaming 9 | 10 | namespace DebugToolkit.Commands 11 | { 12 | // ReSharper disable once UnusedMember.Global 13 | internal static class Command_Noclip 14 | { 15 | internal static bool IsActivated; 16 | 17 | private static NetworkUser _currentNetworkUser; 18 | private static CharacterBody _currentBody; 19 | 20 | internal static void InitRPC() 21 | { 22 | NetworkManager.DebugToolKitComponents.AddComponent<NoclipNet>(); 23 | } 24 | 25 | internal static void InternalToggle(bool shouldLog) 26 | { 27 | if (PlayerCommands.UpdateCurrentPlayerBody(out _currentNetworkUser, out _currentBody)) 28 | { 29 | var kcm = _currentBody.GetComponent<KinematicCharacterMotor>(); 30 | var rigid = _currentBody.GetComponent<Rigidbody>(); 31 | var motor = _currentBody.characterMotor; 32 | if (IsActivated) 33 | { 34 | if (kcm) 35 | { 36 | kcm.RebuildCollidableLayers(); 37 | } 38 | else if (rigid) 39 | { 40 | var collider = rigid.GetComponent<Collider>(); 41 | if (collider) 42 | { 43 | collider.isTrigger = false; 44 | } 45 | } 46 | if (motor) 47 | { 48 | motor.useGravity = motor.gravityParameters.CheckShouldUseGravity(); 49 | } 50 | } 51 | else 52 | { 53 | if (kcm) 54 | { 55 | kcm.CollidableLayers = 0; 56 | } 57 | else if (rigid) 58 | { 59 | var collider = rigid.GetComponent<Collider>(); 60 | if (collider) 61 | { 62 | collider.isTrigger = true; 63 | } 64 | } 65 | if (motor) 66 | { 67 | motor.useGravity = false; 68 | } 69 | } 70 | } 71 | IsActivated = !IsActivated; 72 | if (shouldLog) 73 | { 74 | Log.Message(String.Format(IsActivated ? Lang.SETTING_ENABLED : Lang.SETTING_DISABLED, "noclip")); 75 | } 76 | } 77 | 78 | internal static void Update() 79 | { 80 | if (PlayerCommands.UpdateCurrentPlayerBody(out _currentNetworkUser, out _currentBody)) 81 | Loop(); 82 | } 83 | 84 | private static void Loop() 85 | { 86 | var kcm = _currentBody.GetComponent<KinematicCharacterMotor>(); 87 | var rigid = _currentBody.GetComponent<Rigidbody>(); 88 | if (kcm) // when respawning or things like that, call the toggle to set the variables correctly again 89 | { 90 | if (kcm.CollidableLayers != 0) 91 | { 92 | InternalToggle(false); 93 | InternalToggle(false); 94 | } 95 | } 96 | else if (rigid) 97 | { 98 | var collider = rigid.GetComponent<Collider>(); 99 | if (collider && !collider.isTrigger) 100 | { 101 | InternalToggle(false); 102 | InternalToggle(false); 103 | } 104 | } 105 | 106 | var inputBank = _currentBody.GetComponent<InputBankTest>(); 107 | var motor = _currentBody.characterMotor; 108 | if (inputBank && (motor || rigid)) 109 | { 110 | var forwardDirection = inputBank.moveVector.normalized; 111 | var aimDirection = inputBank.aimDirection.normalized; 112 | var isForward = Vector3.Dot(forwardDirection, aimDirection) > 0f; 113 | 114 | var isSprinting = _currentNetworkUser.inputPlayer.GetButton("Sprint"); 115 | // ReSharper disable once CompareOfFloatsByEqualityOperator 116 | var isStrafing = _currentNetworkUser.inputPlayer.GetAxis("MoveVertical") != 0f; 117 | var scalar = isSprinting ? 100f : 50f; 118 | 119 | var velocity = forwardDirection * scalar; 120 | if (isStrafing) 121 | { 122 | velocity.y = aimDirection.y * (isForward ? scalar : -scalar); 123 | } 124 | if (inputBank.jump.down) 125 | { 126 | velocity.y = 50f; 127 | } 128 | 129 | if (motor) 130 | { 131 | motor.velocity = velocity; 132 | } 133 | else 134 | { 135 | rigid.velocity = velocity; 136 | } 137 | } 138 | } 139 | 140 | internal static void DisableOnStopClient(On.RoR2.Networking.NetworkManagerSystem.orig_OnStopClient orig, RoR2.Networking.NetworkManagerSystem self) 141 | { 142 | if (IsActivated) 143 | { 144 | InternalToggle(true); 145 | } 146 | orig(self); 147 | } 148 | 149 | // ReSharper disable once InconsistentNaming 150 | internal static void DisableOOBCheck(On.RoR2.MapZone.orig_TeleportBody orig, MapZone self, CharacterBody characterBody) 151 | { 152 | if (!IsActivated || !characterBody.isPlayerControlled) 153 | orig(self, characterBody); 154 | } 155 | 156 | internal static void DisableOnRunDestroy(Run run) 157 | { 158 | if (IsActivated) 159 | { 160 | InternalToggle(true); 161 | } 162 | } 163 | } 164 | 165 | 166 | // ReSharper disable once ClassNeverInstantiated.Global 167 | // ReSharper disable once MemberCanBeMadeStatic.Local 168 | // ReSharper disable once UnusedParameter.Local 169 | internal class NoclipNet : NetworkBehaviour 170 | { 171 | private static NoclipNet _instance; 172 | 173 | private void Awake() 174 | { 175 | _instance = this; 176 | } 177 | 178 | internal static void Invoke(NetworkUser argsSender) 179 | { 180 | _instance.TargetToggle(argsSender.connectionToClient); 181 | } 182 | 183 | [TargetRpc] 184 | private void TargetToggle(NetworkConnection _) 185 | { 186 | Command_Noclip.InternalToggle(true); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Code/DT-Commands/Command_Teleport.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | using UnityEngine; 3 | using UnityEngine.Networking; 4 | 5 | // ReSharper disable InconsistentNaming 6 | 7 | namespace DebugToolkit.Commands 8 | { 9 | public static class Command_Teleport 10 | { 11 | private static CharacterBody _currentBody; 12 | 13 | internal static void InitRPC() 14 | { 15 | NetworkManager.DebugToolKitComponents.AddComponent<TeleportNet>(); 16 | } 17 | 18 | internal static void InternalActivation() 19 | { 20 | if (PlayerCommands.UpdateCurrentPlayerBody(out _, out _currentBody)) 21 | { 22 | var inputBank = _currentBody.inputBank; 23 | if (inputBank) 24 | { 25 | if (Physics.Raycast(inputBank.aimOrigin, inputBank.aimDirection, out var hit, Mathf.Infinity, 1 << 11)) 26 | { 27 | var footOffset = _currentBody.transform.position - _currentBody.footPosition; 28 | TeleportHelper.TeleportGameObject(_currentBody.gameObject, hit.point + footOffset); 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | // ReSharper disable once ClassNeverInstantiated.Global 36 | // ReSharper disable once MemberCanBeMadeStatic.Local 37 | // ReSharper disable once UnusedParameter.Local 38 | // ReSharper disable once UnusedMember.Local 39 | internal class TeleportNet : NetworkBehaviour 40 | { 41 | private static TeleportNet _instance; 42 | 43 | private void Awake() 44 | { 45 | _instance = this; 46 | } 47 | 48 | internal static void Invoke(NetworkUser argsSender) 49 | { 50 | _instance.TargetToggle(argsSender.connectionToClient); 51 | } 52 | 53 | [TargetRpc] 54 | private void TargetToggle(NetworkConnection _) 55 | { 56 | Command_Teleport.InternalActivation(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Code/DT-Commands/DEBUG.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | using System.Text; 3 | 4 | namespace DebugToolkit.Commands 5 | { 6 | #if DEBUG 7 | class DEBUG 8 | { 9 | [ConCommand(commandName = "network_echo", flags = ConVarFlags.ExecuteOnServer, helpText = "Sends a message to the target network user.")] 10 | private static void CCNetworkEcho(ConCommandArgs args) 11 | { 12 | args.CheckArgumentCount(2); 13 | Log.Target target = (Log.Target)args.GetArgInt(0); 14 | 15 | //Some fancyspancy thing that concatenates all remaining arguments as a single string. 16 | StringBuilder s = new StringBuilder(); 17 | args.userArgs.RemoveAt(0); 18 | args.userArgs.ForEach((string temp) => { s.Append(temp + " "); }); 19 | string str = s.ToString().TrimEnd(' '); 20 | 21 | Log.Message(str, Log.LogLevel.Message, target); 22 | } 23 | 24 | [ConCommand(commandName = "getItemName", flags = ConVarFlags.None, helpText = "Match a partial localised item name to an ItemIndex")] 25 | private static void CCGetItemName(ConCommandArgs args) 26 | { 27 | Log.Message(StringFinder.Instance.GetItemFromPartial(args[0]).ToString()); 28 | } 29 | 30 | [ConCommand(commandName = "getBodyName", flags = ConVarFlags.None, helpText = "Match a partial localised body name to a character body name")] 31 | private static void CCGetBodyName(ConCommandArgs args) 32 | { 33 | Log.Message(StringFinder.Instance.GetBodyFromPartial(args[0])); 34 | } 35 | 36 | [ConCommand(commandName = "getEquipName", flags = ConVarFlags.None, helpText = "Match a partial localised equip name to an EquipIndex")] 37 | private static void CCGetEquipName(ConCommandArgs args) 38 | { 39 | Log.Message(StringFinder.Instance.GetEquipFromPartial(args[0]).ToString()); 40 | } 41 | 42 | [ConCommand(commandName = "getMasterName", flags = ConVarFlags.None, helpText = "Match a partial localised Master name to a CharacterMaster")] 43 | private static void CCGetMasterName(ConCommandArgs args) 44 | { 45 | Log.Message(StringFinder.Instance.GetAiFromPartial(args[0])); 46 | } 47 | 48 | [ConCommand(commandName = "getTeamIndexPartial", flags = ConVarFlags.None, helpText = "Match a partial TeamIndex")] 49 | private static void CCGetTeamIndexPartial(ConCommandArgs args) 50 | { 51 | //Alias.Instance.GetMasterName(args[0]); 52 | Log.Message(StringFinder.GetEnumFromPartial<TeamIndex>(args[0]).ToString()); 53 | } 54 | 55 | [ConCommand(commandName = "getDirectorCardPartial", flags = ConVarFlags.None, helpText = "Match a partial DirectorCard")] 56 | private static void CCGetDirectorCardPartial(ConCommandArgs args) 57 | { 58 | Log.Message(StringFinder.Instance.GetDirectorCardFromPartial(args[0]).spawnCard.prefab.name); 59 | } 60 | 61 | [ConCommand(commandName = "list_family", flags = ConVarFlags.ExecuteOnServer, helpText = "Lists all monster families in the current stage.")] 62 | private static void CCListFamily(ConCommandArgs args) 63 | { 64 | StringBuilder s = new StringBuilder(); 65 | foreach (ClassicStageInfo.MonsterFamily family in ClassicStageInfo.instance.possibleMonsterFamilies) 66 | { 67 | s.AppendLine(family.familySelectionChatString); 68 | } 69 | Log.MessageNetworked(s.ToString(), args, Log.LogLevel.MessageClientOnly); 70 | } 71 | 72 | [ConCommand(commandName = "list_pcmc", flags = ConVarFlags.None, helpText = "Lists all PlayerCharacterMasterController instances.")] 73 | private static void CCListPlayerCharacterMasterController(ConCommandArgs args) 74 | { 75 | StringBuilder sb = new StringBuilder(); 76 | sb.AppendLine("Number of PCMC instances : " + PlayerCharacterMasterController.instances.Count); 77 | foreach (var masterController in PlayerCharacterMasterController.instances) 78 | { 79 | sb.AppendLine($" is connected : {masterController.isConnected}"); 80 | } 81 | if (args.sender == null) 82 | { 83 | Log.Message(sb.ToString()); 84 | } 85 | else 86 | { 87 | Log.MessageNetworked(sb.ToString(), args, Log.LogLevel.MessageClientOnly); 88 | } 89 | 90 | } 91 | 92 | } 93 | #endif 94 | } 95 | -------------------------------------------------------------------------------- /Code/DT-Commands/Lists.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using UnityEngine.Networking; 5 | using static DebugToolkit.Log; 6 | 7 | namespace DebugToolkit.Commands 8 | { 9 | class Lists 10 | { 11 | [ConCommand(commandName = "list_interactables", flags = ConVarFlags.None, helpText = Lang.LISTINTERACTABLE_HELP)] 12 | [ConCommand(commandName = "list_interactibles", flags = ConVarFlags.None, helpText = Lang.LISTINTERACTABLE_HELP)] 13 | [AutoComplete(Lang.LISTQUERY_ARGS)] 14 | private static void CCListInteractables(ConCommandArgs args) 15 | { 16 | StringBuilder sb = new StringBuilder(); 17 | var arg = args.Count > 0 ? args[0] : ""; 18 | var cards = new HashSet<InteractableSpawnCard>(StringFinder.Instance.GetInteractableSpawnCardsFromPartial(arg)); 19 | for (int i = 0; i < StringFinder.Instance.InteractableSpawnCards.Count; i++) 20 | { 21 | var isc = StringFinder.Instance.InteractableSpawnCards[i]; 22 | if (cards.Contains(isc)) 23 | { 24 | var langInvar = StringFinder.GetLangInvar(StringFinder.GetInteractableName(isc.prefab)); 25 | sb.AppendLine($"[{i}]{isc.name}={langInvar}"); 26 | } 27 | } 28 | var s = sb.Length > 0 ? sb.ToString().TrimEnd('\n') : string.Format(Lang.NOMATCH_ERROR, "interactables", arg); 29 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 30 | } 31 | 32 | [ConCommand(commandName = "list_player", flags = ConVarFlags.None, helpText = Lang.LISTPLAYER_HELP)] 33 | [AutoComplete(Lang.LISTQUERY_ARGS)] 34 | private static void CCListPlayer(ConCommandArgs args) 35 | { 36 | StringBuilder sb = new StringBuilder(); 37 | var arg = args.Count > 0 ? args[0] : ""; 38 | var players = new HashSet<NetworkUser>(StringFinder.Instance.GetPlayersFromPartial(arg)); 39 | for (int i = 0; i < NetworkUser.readOnlyInstancesList.Count; i++) 40 | { 41 | var user = NetworkUser.readOnlyInstancesList[i]; 42 | if (players.Contains(user)) 43 | { 44 | sb.AppendLine($"[{i}]{user.userName}"); 45 | } 46 | } 47 | if (sb.Length > 0) 48 | { 49 | Log.MessageNetworked(sb.ToString().TrimEnd('\n'), args, LogLevel.MessageClientOnly); 50 | } 51 | else 52 | { 53 | var s = NetworkClient.active ? string.Format(Lang.NOMATCH_ERROR, "players", arg) : Lang.NOCONNECTION_ERROR; 54 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 55 | } 56 | } 57 | 58 | [ConCommand(commandName = "list_artifact", flags = ConVarFlags.None, helpText = Lang.LISTARTIFACT_HELP)] 59 | private static void CCListArtifact(ConCommandArgs args) 60 | { 61 | StringBuilder sb = new StringBuilder(); 62 | var arg = args.Count > 0 ? args[0] : ""; 63 | var indices = StringFinder.Instance.GetArtifactsFromPartial(arg); 64 | foreach (var index in indices) 65 | { 66 | var artifact = ArtifactCatalog.GetArtifactDef(index); 67 | var langInvar = StringFinder.GetLangInvar(artifact.nameToken); 68 | sb.AppendLine($"[{(int)index}]{artifact.cachedName}={langInvar}"); 69 | } 70 | var s = sb.Length > 0 ? sb.ToString().TrimEnd('\n') : string.Format(Lang.NOMATCH_ERROR, "artifacts", arg); 71 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 72 | } 73 | 74 | [ConCommand(commandName = "list_ai", flags = ConVarFlags.None, helpText = Lang.LISTAI_HELP)] 75 | [AutoComplete(Lang.LISTQUERY_ARGS)] 76 | private static void CCListAI(ConCommandArgs args) 77 | { 78 | StringBuilder sb = new StringBuilder(); 79 | var arg = args.Count > 0 ? args[0] : ""; 80 | var indices = StringFinder.Instance.GetAisFromPartial(arg); 81 | foreach (var index in indices) 82 | { 83 | var master = MasterCatalog.GetMasterPrefab(index).GetComponent<CharacterMaster>(); 84 | var langInvar = StringFinder.GetLangInvar(StringFinder.GetMasterName(master)); 85 | sb.AppendLine($"[{(int)index}]{master.name}={langInvar}"); 86 | } 87 | var s = sb.Length > 0 ? sb.ToString().TrimEnd('\n') : string.Format(Lang.NOMATCH_ERROR, "masters", arg); 88 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 89 | } 90 | 91 | [ConCommand(commandName = "list_body", flags = ConVarFlags.None, helpText = Lang.LISTBODY_HELP)] 92 | [AutoComplete(Lang.LISTQUERY_ARGS)] 93 | private static void CCListBody(ConCommandArgs args) 94 | { 95 | StringBuilder sb = new StringBuilder(); 96 | var arg = args.Count > 0 ? args[0] : ""; 97 | var indices = StringFinder.Instance.GetBodiesFromPartial(arg); 98 | foreach (var index in indices) 99 | { 100 | var body = BodyCatalog.GetBodyPrefabBodyComponent(index); 101 | var langInvar = StringFinder.GetLangInvar(body.baseNameToken); 102 | sb.AppendLine($"[{(int)index}]{body.name}={langInvar}"); 103 | } 104 | var s = sb.Length > 0 ? sb.ToString().TrimEnd('\n') : string.Format(Lang.NOMATCH_ERROR, "bodies", arg); 105 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 106 | } 107 | 108 | [ConCommand(commandName = "list_survivor", flags = ConVarFlags.None, helpText = Lang.LISTSURVIVOR_HELP)] 109 | [AutoComplete(Lang.LISTQUERY_ARGS)] 110 | private static void CCListSurvivor(ConCommandArgs args) 111 | { 112 | StringBuilder sb = new StringBuilder(); 113 | var arg = args.Count > 0 ? args[0] : ""; 114 | var indices = StringFinder.Instance.GetSurvivorsFromPartial(arg); 115 | foreach (var index in indices) 116 | { 117 | var survivor = SurvivorCatalog.GetSurvivorDef(index); 118 | var langInvar = StringFinder.GetLangInvar(survivor.displayNameToken); 119 | var bodyName = survivor.bodyPrefab?.name ?? StringFinder.NAME_NOTFOUND; 120 | var body = survivor.bodyPrefab?.GetComponent<CharacterBody>(); 121 | var masterIndex = body ? MasterCatalog.FindAiMasterIndexForBody(body.bodyIndex) : MasterCatalog.MasterIndex.none; 122 | var masterName = MasterCatalog.GetMasterPrefab(masterIndex)?.name ?? StringFinder.NAME_NOTFOUND; 123 | sb.AppendLine($"[{(int)index}]{survivor.cachedName}={langInvar} ({bodyName}, {masterName})"); 124 | } 125 | var s = sb.Length > 0 ? sb.ToString().TrimEnd('\n') : string.Format(Lang.NOMATCH_ERROR, "survivors", arg); 126 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 127 | } 128 | 129 | [ConCommand(commandName = "list_elite", flags = ConVarFlags.None, helpText = Lang.LISTELITE_HELP)] 130 | [AutoComplete(Lang.LISTQUERY_ARGS)] 131 | private static void CCListElites(ConCommandArgs args) 132 | { 133 | StringBuilder sb = new StringBuilder(); 134 | var arg = args.Count > 0 ? args[0] : ""; 135 | var indices = StringFinder.Instance.GetElitesFromPartial(arg); 136 | foreach (var index in indices) 137 | { 138 | var elite = EliteCatalog.GetEliteDef(index); 139 | var name = elite?.name ?? "None"; 140 | var langInvar = StringFinder.GetLangInvar(elite?.modifierToken).Replace("{0}", ""); 141 | sb.AppendLine($"[{(int)index}]{name}={langInvar}"); 142 | } 143 | var s = sb.Length > 0 ? sb.ToString().TrimEnd('\n') : string.Format(Lang.NOMATCH_ERROR, "elites", arg); 144 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 145 | } 146 | 147 | [ConCommand(commandName = "list_team", flags = ConVarFlags.None, helpText = Lang.LISTTEAM_HELP)] 148 | [AutoComplete(Lang.LISTQUERY_ARGS)] 149 | private static void CCListTeams(ConCommandArgs args) 150 | { 151 | StringBuilder sb = new StringBuilder(); 152 | var arg = args.Count > 0 ? args[0] : ""; 153 | var indices = StringFinder.Instance.GetTeamsFromPartial(arg); 154 | foreach (var index in indices) 155 | { 156 | sb.AppendLine($"[{(int)index}]{index}"); 157 | } 158 | var s = sb.Length > 0 ? sb.ToString().TrimEnd('\n') : string.Format(Lang.NOMATCH_ERROR, "teams", arg); 159 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 160 | } 161 | 162 | [ConCommand(commandName = "list_directorcards", flags = ConVarFlags.None, helpText = Lang.LISTDIRECTORCARDS_HELP)] 163 | [AutoComplete(Lang.LISTQUERY_ARGS)] 164 | private static void CCListDirectorCards(ConCommandArgs args) 165 | { 166 | StringBuilder sb = new StringBuilder(); 167 | var arg = args.Count > 0 ? args[0] : ""; 168 | var cards = new HashSet<DirectorCard>(StringFinder.Instance.GetDirectorCardsFromPartial(arg)); 169 | for (int i = 0; i < StringFinder.Instance.DirectorCards.Count; i++) 170 | { 171 | var card = StringFinder.Instance.DirectorCards[i]; 172 | if (cards.Contains(card)) 173 | { 174 | var langInvar = StringFinder.GetLangInvar(StringFinder.GetMasterName(card.spawnCard.prefab.GetComponent<CharacterMaster>())); 175 | sb.AppendLine($"[{i}]{card.spawnCard.name}={langInvar}"); 176 | } 177 | } 178 | var s = sb.Length > 0 ? sb.ToString().TrimEnd('\n') : string.Format(Lang.NOMATCH_ERROR, "director cards", arg); 179 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 180 | } 181 | 182 | [ConCommand(commandName = "list_scene", flags = ConVarFlags.None, helpText = Lang.LISTSCENE_HELP)] 183 | [AutoComplete(Lang.LISTQUERY_ARGS)] 184 | private static void CCListScene(ConCommandArgs args) 185 | { 186 | StringBuilder sb = new StringBuilder(); 187 | var arg = args.Count > 0 ? args[0] : ""; 188 | var indices = StringFinder.Instance.GetScenesFromPartial(arg, true); 189 | foreach (var index in indices) 190 | { 191 | var scene = SceneCatalog.GetSceneDef(index); 192 | var langInvar = StringFinder.GetLangInvar(scene.nameToken); 193 | sb.AppendLine($"[{index}]{scene.cachedName}={langInvar} (offline={scene.isOfflineScene})"); 194 | } 195 | var s = sb.Length > 0 ? sb.ToString().TrimEnd('\n') : string.Format(Lang.NOMATCH_ERROR, "scenes", arg); 196 | Log.MessageNetworked(s, args, LogLevel.MessageClientOnly); 197 | } 198 | 199 | [ConCommand(commandName = "list_skin", flags = ConVarFlags.None, helpText = Lang.LISTSKIN_HELP)] 200 | [AutoComplete(Lang.LISTSKIN_ARGS)] 201 | private static void CCListSkin(ConCommandArgs args) 202 | { 203 | //string langInvar; 204 | StringBuilder sb = new StringBuilder(); 205 | if (args.Count == 0) 206 | { 207 | args.userArgs.Add(Lang.ALL); //simple 208 | } 209 | if (args.Count >= 1) 210 | { 211 | string bodyName = args[0]; 212 | string upperBodyName = bodyName.ToUpperInvariant(); 213 | 214 | switch (upperBodyName) 215 | { 216 | case "BODY": 217 | foreach (var bodyComponent in BodyCatalog.allBodyPrefabBodyBodyComponents) 218 | { 219 | AppendSkinIndices(sb, bodyComponent); 220 | } 221 | break; 222 | case Lang.ALL: 223 | HashSet<SkinDef> skinDefs = new HashSet<SkinDef>(); 224 | foreach (var skin in SkinCatalog.allSkinDefs) 225 | { 226 | if (skinDefs.Contains(skin)) 227 | continue; 228 | skinDefs.Add(skin); 229 | var langInvar = StringFinder.GetLangInvar(skin.nameToken); 230 | sb.AppendLine($"[{skin.skinIndex}] {skin.name}={langInvar}"); 231 | } 232 | break; 233 | default: 234 | CharacterBody body; 235 | if (upperBodyName == "SELF") 236 | { 237 | if (args.sender == null) 238 | { 239 | Log.Message("Can't choose self if not in-game!", LogLevel.Error); 240 | return; 241 | } 242 | if (args.senderBody) 243 | { 244 | body = args.senderBody; 245 | } 246 | else 247 | { 248 | if (args.senderMaster && args.senderMaster.bodyPrefab) 249 | { 250 | body = args.senderMaster.bodyPrefab.GetComponent<CharacterBody>(); 251 | } 252 | else 253 | { 254 | body = BodyCatalog.GetBodyPrefabBodyComponent(args.sender.bodyIndexPreference); 255 | // a little redundant 256 | } 257 | } 258 | } 259 | else 260 | { 261 | var bodyIndex = StringFinder.Instance.GetBodyFromPartial(args[0]); 262 | if (bodyIndex == BodyIndex.None) 263 | { 264 | Log.MessageNetworked("Please use list_body to print CharacterBodies", args, LogLevel.MessageClientOnly); 265 | return; 266 | } 267 | body = BodyCatalog.GetBodyPrefabBodyComponent(bodyIndex); 268 | } 269 | if (body) 270 | { 271 | AppendSkinIndices(sb, body); 272 | } 273 | else 274 | { 275 | Log.MessageNetworked("Please use list_body to print CharacterBodies", args, LogLevel.MessageClientOnly); 276 | return; 277 | } 278 | break; 279 | } 280 | } 281 | 282 | Log.MessageNetworked(sb.ToString(), args, LogLevel.MessageClientOnly); 283 | } 284 | 285 | private static void AppendSkinIndices(StringBuilder stringBuilder, CharacterBody body) 286 | { 287 | var skins = BodyCatalog.GetBodySkins(body.bodyIndex); 288 | if (skins.Length > 0) 289 | { 290 | var langInvar = StringFinder.GetLangInvar(body.baseNameToken); 291 | stringBuilder.AppendLine($"[{body.bodyIndex}]{body.name}={langInvar}"); 292 | int i = 0; 293 | foreach (var skinDef in skins) 294 | { 295 | langInvar = StringFinder.GetLangInvar(skinDef.nameToken); 296 | stringBuilder.AppendLine($"\t[{i}={skinDef.skinIndex}] {skinDef.name}={langInvar}"); 297 | i++; 298 | } 299 | } 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /Code/DT-Commands/LobbyManagement.cs: -------------------------------------------------------------------------------- 1 | using DebugToolkit.Permissions; 2 | using RoR2; 3 | using RoR2.Networking; 4 | using UnityEngine; 5 | using UnityEngine.Networking; 6 | using static DebugToolkit.Log; 7 | 8 | namespace DebugToolkit.Commands 9 | { 10 | class LobbyManagement 11 | { 12 | [ConCommand(commandName = "kick", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.KICK_HELP)] 13 | [AutoComplete(Lang.KICK_ARGS)] 14 | [RequiredLevel] 15 | private static void CCKick(ConCommandArgs args) 16 | { 17 | if (args.Count == 0) 18 | { 19 | Log.MessageNetworked(Lang.INSUFFICIENT_ARGS + Lang.KICK_ARGS, args, LogLevel.Error); 20 | return; 21 | } 22 | var client = GetClientFromArgs(args); 23 | if (client == null) 24 | { 25 | return; 26 | } 27 | var reason = new NetworkManagerSystem.SimpleLocalizedKickReason("KICK_REASON_KICK"); 28 | NetworkManagerSystem.singleton.ServerKickClient(client, reason); 29 | } 30 | 31 | [ConCommand(commandName = "ban", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.BAN_HELP)] 32 | [AutoComplete(Lang.BAN_ARGS)] 33 | [RequiredLevel] 34 | private static void CCBan(ConCommandArgs args) 35 | { 36 | if (args.Count == 0) 37 | { 38 | Log.MessageNetworked(Lang.INSUFFICIENT_ARGS + Lang.BAN_ARGS, args, LogLevel.Error); 39 | return; 40 | } 41 | var client = GetClientFromArgs(args); 42 | if (client == null) 43 | { 44 | return; 45 | } 46 | NetworkManagerSystem.singleton.ServerBanClient(client); 47 | } 48 | 49 | [ConCommand(commandName = "true_kill", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.TRUEKILL_HELP)] 50 | [AutoComplete(Lang.TRUEKILL_ARGS)] 51 | private static void CCTrueKill(ConCommandArgs args) 52 | { 53 | if (args.sender == null && (args.Count < 1 || args[0] == Lang.DEFAULT_VALUE)) 54 | { 55 | Log.Message(Lang.INSUFFICIENT_ARGS + Lang.TRUEKILL_ARGS, LogLevel.Error); 56 | return; 57 | } 58 | CharacterMaster master = args.sender?.master; 59 | if (args.Count > 0) 60 | { 61 | NetworkUser player = Util.GetNetUserFromString(args.userArgs); 62 | if (player == null) 63 | { 64 | Log.MessageNetworked(Lang.PLAYER_NOTFOUND, args, LogLevel.MessageClientOnly); 65 | return; 66 | } 67 | master = player.master; 68 | } 69 | 70 | master.TrueKill(); 71 | Log.MessageNetworked(master.name + " was killed by server.", args); 72 | } 73 | 74 | private static NetworkConnection GetClientFromArgs(ConCommandArgs args) 75 | { 76 | NetworkUser nu = Util.GetNetUserFromString(args.userArgs); 77 | if (nu == null) 78 | { 79 | Log.MessageNetworked(Lang.PLAYER_NOTFOUND, args, LogLevel.Error); 80 | return null; 81 | } 82 | 83 | // Check if we can kick targeted user. 84 | if (!Application.isBatchMode) 85 | { 86 | foreach (var serverLocalUsers in NetworkUser.readOnlyLocalPlayersList) 87 | { 88 | if (serverLocalUsers == nu) 89 | { 90 | Log.MessageNetworked("Specified user is hosting.", args, LogLevel.Error); 91 | return null; 92 | } 93 | } 94 | } 95 | else if (PermissionSystem.IsEnabled.Value) 96 | { 97 | if (!PermissionSystem.HasMorePerm(args.sender, nu, args)) 98 | { 99 | Log.MessageNetworked("The target has a higher permission level that you.", args, LogLevel.Error); 100 | return null; 101 | } 102 | } 103 | 104 | NetworkConnection client = null; 105 | foreach (var connection in NetworkServer.connections) 106 | { 107 | if (nu.connectionToClient == connection) 108 | { 109 | client = connection; 110 | break; 111 | } 112 | } 113 | 114 | if (client == null) 115 | { 116 | Log.MessageNetworked("Error trying to find the associated connection with the user", args, LogLevel.Error); 117 | return null; 118 | } 119 | 120 | return client; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Code/DT-Commands/Macros.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | 3 | namespace DebugToolkit.Commands 4 | { 5 | static class Macros 6 | { 7 | [ConCommand(commandName = "midgame", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.MACRO_MIDGAME_HELP)] 8 | private static void Midgame(ConCommandArgs args) 9 | { 10 | NetworkUser a = args.sender; 11 | Invoke(a, "fixed_time", "1325"); 12 | Invoke(a, "run_set_stages_cleared", "5"); 13 | Invoke(a, "team_set_level", "1", "15"); 14 | foreach (NetworkUser user in NetworkUser.readOnlyInstancesList) 15 | { 16 | Invoke(a, "remove_all_items", user.userName); 17 | Invoke(a, "random_items", "23", "Tier1:100,Tier2:60,Tier3:4", user.userName); 18 | Invoke(a, "give_equip", "random", user.userName); 19 | } 20 | Invoke(a, "set_scene", "bazaar"); 21 | } 22 | 23 | [ConCommand(commandName = "lategame", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.MACRO_LATEGAME_HELP)] 24 | private static void LateGame(ConCommandArgs args) 25 | { 26 | NetworkUser a = args.sender; 27 | Invoke(a, "fixed_time", "3420"); 28 | Invoke(a, "run_set_stages_cleared", "8"); 29 | Invoke(a, "team_set_level", "1", "24"); 30 | foreach (NetworkUser user in NetworkUser.readOnlyInstancesList) 31 | { 32 | Invoke(a, "remove_all_items", user.userName); 33 | Invoke(a, "random_items", "75", "Tier1:100,Tier2:60,Tier3:4", user.userName); 34 | Invoke(a, "give_equip", "random", user.userName); 35 | } 36 | Invoke(a, "set_scene", "bazaar"); 37 | } 38 | 39 | [ConCommand(commandName = "dtzoom", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.MACRO_DTZOOM_HELP)] 40 | private static void Zoom(ConCommandArgs args) 41 | { 42 | Invoke(args.sender, "give_item", "hoof", "20"); 43 | Invoke(args.sender, "give_item", "feather", "200"); 44 | } 45 | 46 | private static void Invoke(NetworkUser user, string commandname, params string[] args) 47 | { 48 | DebugToolkit.InvokeCMD(user, commandname, args); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Code/DT-Commands/Miscellaneous.cs: -------------------------------------------------------------------------------- 1 | using BepInEx.Bootstrap; 2 | using RoR2; 3 | 4 | namespace DebugToolkit.Commands 5 | { 6 | class Miscellaneous 7 | { 8 | [ConCommand(commandName = "post_sound_event", flags = ConVarFlags.None, helpText = Lang.POSTSOUNDEVENT_HELP)] 9 | [AutoComplete(Lang.POSTSOUNDEVENT_ARGS)] 10 | private static void CCPostSoundEvent(ConCommandArgs args) 11 | { 12 | // Hack to not substitute the value of the constant as the DS game version has a different value 13 | if ((bool)typeof(RoR2Application).GetField("isDedicatedServer").GetValue(RoR2Application.instance)) 14 | { 15 | Log.MessageWarning(Lang.DS_NOTAVAILABLE); 16 | return; 17 | } 18 | if (args.Count == 0) 19 | { 20 | Log.Message(Lang.INSUFFICIENT_ARGS + Lang.POSTSOUNDEVENT_ARGS); 21 | return; 22 | } 23 | uint result; 24 | if (TextSerialization.TryParseInvariant(args[0], out uint eventId)) 25 | { 26 | result = AkSoundEngine.PostEvent(eventId, CameraRigController.readOnlyInstancesList[0].gameObject); 27 | } 28 | else 29 | { 30 | result = AkSoundEngine.PostEvent(args[0], CameraRigController.readOnlyInstancesList[0].gameObject); 31 | } 32 | if (result == 0) 33 | { 34 | Log.Message("Sound not found."); 35 | } 36 | } 37 | 38 | [ConCommand(commandName = "reload_all_config", flags = ConVarFlags.None, helpText = Lang.RELOADCONFIG_HELP)] 39 | private static void CCReloadAllConfig(ConCommandArgs args) 40 | { 41 | foreach (var pluginInfo in Chainloader.PluginInfos.Values) 42 | { 43 | try 44 | { 45 | // Will this even fail with the null conditional operator? 46 | pluginInfo.Instance.Config?.Reload(); 47 | } 48 | catch 49 | { 50 | Log.MessageNetworked($"The config file for {pluginInfo} doesn't exist or has a custom name.", args, Log.LogLevel.Warning); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Code/DT-Commands/Money.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Runtime.CompilerServices; 5 | using UnityEngine; 6 | using static DebugToolkit.Log; 7 | 8 | namespace DebugToolkit.Commands 9 | { 10 | class Money 11 | { 12 | [ConCommand(commandName = "give_lunar", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.GIVELUNAR_HELP)] 13 | [AutoComplete(Lang.GIVELUNAR_ARGS)] 14 | private static void CCGiveLunar(ConCommandArgs args) 15 | { 16 | if (!Run.instance) 17 | { 18 | Log.MessageNetworked(Lang.NOTINARUN_ERROR, args, LogLevel.MessageClientOnly); 19 | return; 20 | } 21 | if (args.sender == null) 22 | { 23 | Log.Message("Can't modify Lunar coins of other users directly.", LogLevel.MessageClientOnly); 24 | return; 25 | } 26 | int amount = 1; 27 | if (args.Count > 0 && args[0] != Lang.DEFAULT_VALUE && !TextSerialization.TryParseInvariant(args[0], out amount)) 28 | { 29 | Log.MessageNetworked(String.Format(Lang.PARSE_ERROR, "amount", "int"), args, LogLevel.MessageClientOnly); 30 | return; 31 | } 32 | string str = "Nothing happened. Big surprise."; 33 | NetworkUser target = args.sender; 34 | if (amount > 0) 35 | { 36 | target.AwardLunarCoins((uint)amount); 37 | str = string.Format(Lang.GIVELUNAR_2, "Gave", amount); 38 | } 39 | if (amount < 0) 40 | { 41 | amount *= -1; 42 | target.DeductLunarCoins((uint)(amount)); 43 | str = string.Format(Lang.GIVELUNAR_2, "Removed", amount); 44 | } 45 | Log.MessageNetworked(str, args); 46 | } 47 | 48 | [ConCommand(commandName = "give_money", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.GIVEMONEY_HELP)] 49 | [AutoComplete(Lang.GIVEMONEY_ARGS)] 50 | private static void CCGiveMoney(ConCommandArgs args) 51 | { 52 | if (!Run.instance) 53 | { 54 | Log.MessageNetworked(Lang.NOTINARUN_ERROR, args, LogLevel.MessageClientOnly); 55 | return; 56 | } 57 | if (args.Count == 0) 58 | { 59 | Log.MessageNetworked(Lang.INSUFFICIENT_ARGS + Lang.GIVEMONEY_ARGS, args, LogLevel.MessageClientOnly); 60 | return; 61 | } 62 | 63 | if (!TextSerialization.TryParseInvariant(args[0], out int result)) 64 | { 65 | Log.MessageNetworked(string.Format(Lang.PARSE_ERROR, "amount", "int"), args, LogLevel.MessageClientOnly); 66 | return; 67 | } 68 | 69 | if (BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("com.funkfrog_sipondo.sharesuite")) 70 | { 71 | if (UseShareSuite()) 72 | { 73 | ShareSuiteGive(result); 74 | return; 75 | } 76 | } 77 | 78 | if (args.Count > 1 && args[1].ToUpperInvariant() != Lang.ALL && args[1].ToUpperInvariant() != Lang.DEFAULT_VALUE) 79 | { 80 | NetworkUser player = Util.GetNetUserFromString(args.userArgs, 1); 81 | if (player != null) 82 | { 83 | GiveMasterMoney(player.master, result); 84 | } 85 | else 86 | { 87 | Log.MessageNetworked(Lang.PLAYER_NOTFOUND, args, LogLevel.MessageClientOnly); 88 | return; 89 | } 90 | } 91 | else 92 | { 93 | var teamIndex = args.senderMaster == null ? TeamIndex.Player : args.senderMaster.teamIndex; 94 | GiveTeamMoney(teamIndex, result); 95 | } 96 | 97 | Log.MessageNetworked("$$$", args); 98 | } 99 | 100 | private static void GiveMasterMoney(CharacterMaster master, int amount) 101 | { 102 | if (amount > 0) 103 | { 104 | master.GiveMoney((uint)amount); 105 | return; 106 | } 107 | master.money = (uint)Math.Max(0, master.money + amount); 108 | } 109 | 110 | private static void GiveTeamMoney(TeamIndex teamIndex, int money) 111 | { 112 | var players = new List<CharacterMaster>(); 113 | foreach (var member in TeamComponent.GetTeamMembers(teamIndex)) 114 | { 115 | var body = member.body; 116 | if (body && body.isPlayerControlled && body.master) 117 | { 118 | players.Add(body.master); 119 | } 120 | } 121 | int num = players.Count; 122 | if (num != 0) 123 | { 124 | money = Mathf.CeilToInt(money / (float)num); 125 | foreach (var master in players) 126 | { 127 | GiveMasterMoney(master, money); 128 | } 129 | } 130 | } 131 | 132 | [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] 133 | private static void ShareSuiteGive(int amount) 134 | { 135 | var moneyPool = ShareSuite.MoneySharingHooks.SharedMoneyValue; 136 | amount = Math.Max(-moneyPool, amount); 137 | ShareSuite.MoneySharingHooks.AddMoneyExternal(amount); 138 | } 139 | 140 | [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] 141 | private static bool UseShareSuite() 142 | { 143 | return ShareSuite.ShareSuite.MoneyIsShared.Value && ShareSuite.GeneralHooks.IsMultiplayer(); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Code/DT-Commands/Profile.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | using System; 3 | using static DebugToolkit.Log; 4 | 5 | namespace DebugToolkit.Commands 6 | { 7 | internal class Profile 8 | { 9 | private static bool canSaveProfile = true; 10 | 11 | [ConCommand(commandName = "prevent_profile_writing", flags = ConVarFlags.None, helpText = Lang.PREVENT_PROFILE_WRITING_HELP)] 12 | [AutoComplete(Lang.PREVENT_PROFILE_WRITING_ARGS)] 13 | private static void CCPreventProfileWriting(ConCommandArgs args) 14 | { 15 | if (args.Count > 0) 16 | { 17 | if (!Util.TryParseBool(args[0], out var result)) 18 | { 19 | Log.MessageNetworked(string.Format(Lang.PARSE_ERROR, "'flag'", "'bool'"), args, LogLevel.MessageClientOnly); 20 | return; 21 | } 22 | canSaveProfile = !result; 23 | } 24 | Log.MessageNetworked(String.Format(!canSaveProfile ? Lang.SETTING_ENABLED : Lang.SETTING_DISABLED, "Prevent writing"), args); 25 | } 26 | 27 | internal static bool PreventSave(On.RoR2.SaveSystem.orig_Save orig, SaveSystem self, UserProfile data, bool blocking) 28 | { 29 | return canSaveProfile && orig(self, data, blocking); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Code/DT-Commands/Spawners.cs: -------------------------------------------------------------------------------- 1 | using KinematicCharacterController; 2 | using RoR2; 3 | using RoR2.CharacterAI; 4 | using RoR2.Navigation; 5 | using System; 6 | using System.Collections.Generic; 7 | using UnityEngine; 8 | using UnityEngine.AddressableAssets; 9 | using UnityEngine.Networking; 10 | using static DebugToolkit.Log; 11 | 12 | namespace DebugToolkit.Commands 13 | { 14 | class Spawners 15 | { 16 | private static readonly Dictionary<string, GameObject> portals = new Dictionary<string, GameObject>(); 17 | 18 | [ConCommand(commandName = "spawn_interactable", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.SPAWNINTERACTABLE_HELP)] 19 | [ConCommand(commandName = "spawn_interactible", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.SPAWNINTERACTABLE_HELP)] 20 | [AutoComplete(Lang.SPAWNINTERACTABLE_ARGS)] 21 | private static void CCSpawnInteractable(ConCommandArgs args) 22 | { 23 | if (!Run.instance) 24 | { 25 | Log.MessageNetworked(Lang.NOTINARUN_ERROR, args, LogLevel.MessageClientOnly); 26 | return; 27 | } 28 | if (args.sender == null) 29 | { 30 | Log.Message(Lang.DS_NOTYETIMPLEMENTED, LogLevel.Error); 31 | return; 32 | } 33 | if (args.Count == 0) 34 | { 35 | Log.MessageNetworked(Lang.INSUFFICIENT_ARGS + Lang.SPAWNINTERACTABLE_ARGS, args, LogLevel.MessageClientOnly); 36 | return; 37 | } 38 | if (!args.senderBody) 39 | { 40 | Log.MessageNetworked("Can't spawn an object with relation to a dead target.", args, LogLevel.MessageClientOnly); 41 | return; 42 | } 43 | var isc = StringFinder.Instance.GetInteractableSpawnCardFromPartial(args[0]); 44 | if (isc == null) 45 | { 46 | Log.MessageNetworked(Lang.INTERACTABLE_NOTFOUND, args, LogLevel.MessageClientOnly); 47 | return; 48 | } 49 | // Putting interactables with a collider just far enough to not cause any clipping 50 | // or spawn under the character's feet. The few exceptions with MeshCollider aren't 51 | // treated but they aren't much of an issue. 52 | var colliders = isc.prefab.GetComponentsInChildren<Collider>(); 53 | var distance = 0f; 54 | foreach (var collider in colliders) 55 | { 56 | if (!collider.isTrigger && collider.enabled) 57 | { 58 | var box = collider as BoxCollider; 59 | var capsule = collider as CapsuleCollider; 60 | var sphere = collider as SphereCollider; 61 | var scale = collider.transform.lossyScale; 62 | if (box) 63 | { 64 | var x = box.size.x * scale.x; 65 | var y = box.size.y * scale.y; 66 | distance = Mathf.Max(distance, Mathf.Sqrt(x * x + y * y) * 0.5f); 67 | } 68 | else if (capsule) 69 | { 70 | distance = Mathf.Max(distance, capsule.radius); 71 | } 72 | else if (sphere) 73 | { 74 | distance = Mathf.Max(distance, sphere.radius); 75 | } 76 | } 77 | } 78 | var position = args.senderBody.footPosition; 79 | if (distance > 0f) 80 | { 81 | var direction = args.senderBody.inputBank.aimDirection; 82 | position = position + (args.senderBody.radius + distance) * new Vector3(direction.x, 0f, direction.z); 83 | } 84 | var result = isc.DoSpawn(position, new Quaternion(), new DirectorSpawnRequest(isc, null, RoR2Application.rng)); 85 | if (!result.success) 86 | { 87 | Log.MessageNetworked("Failed to spawn interactable.", args, LogLevel.MessageClientOnly); 88 | } 89 | } 90 | 91 | [ConCommand(commandName = "spawn_portal", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.SPAWNPORTAL_HELP)] 92 | [AutoComplete(Lang.SPAWNPORTAL_ARGS)] 93 | private static void CCSpawnPortal(ConCommandArgs args) 94 | { 95 | if (!Run.instance) 96 | { 97 | Log.MessageNetworked(Lang.NOTINARUN_ERROR, args, LogLevel.MessageClientOnly); 98 | return; 99 | } 100 | if (args.sender == null) 101 | { 102 | Log.Message(Lang.DS_NOTYETIMPLEMENTED, LogLevel.Error); 103 | return; 104 | } 105 | if (args.Count == 0) 106 | { 107 | Log.MessageNetworked(Lang.INSUFFICIENT_ARGS + Lang.SPAWNPORTAL_ARGS, args, LogLevel.MessageClientOnly); 108 | return; 109 | } 110 | if (!args.senderBody) 111 | { 112 | Log.MessageNetworked("Can't spawn an object with relation to a dead target.", args, LogLevel.MessageClientOnly); 113 | return; 114 | } 115 | 116 | var portalName = args[0].ToLowerInvariant(); 117 | if (!portals.TryGetValue(portalName, out var portal)) 118 | { 119 | Log.MessageNetworked(string.Format(Lang.INVALID_ARG_VALUE, "portal"), args, LogLevel.MessageClientOnly); 120 | return; 121 | } 122 | var currentScene = Stage.instance.sceneDef; 123 | 124 | if (currentScene.cachedName == "voidraid" && portalName == "deepvoid") 125 | { 126 | portal = StringFinder.Instance.GetInteractableSpawnCardFromPartial("VoidOutroPortal").prefab; 127 | } 128 | var position = args.senderBody.footPosition; 129 | // Some portals spawn into the ground 130 | if (portal.name == "DeepVoidPortal") 131 | { 132 | position.y += 4f; 133 | } 134 | else if (portal.name == "PortalArtifactworld") 135 | { 136 | position.y += 10f; 137 | } 138 | 139 | var gameObject = UnityEngine.Object.Instantiate(portal, position, Quaternion.LookRotation(args.senderBody.characterDirection.forward)); 140 | var exit = gameObject.GetComponent<SceneExitController>(); 141 | // The artifact portal erroneously points to mysteryspace by default 142 | if (portalName == "artifact") 143 | { 144 | exit.destinationScene = SceneCatalog.FindSceneDef("artifactworld"); 145 | } 146 | if (currentScene.cachedName == "voidraid" && gameObject.name.Contains("VoidOutroPortal")) 147 | { 148 | exit.useRunNextStageScene = false; 149 | } 150 | else 151 | { 152 | exit.useRunNextStageScene = exit.destinationScene == currentScene; 153 | } 154 | NetworkServer.Spawn(gameObject); 155 | } 156 | 157 | [ConCommand(commandName = "spawn_ai", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.SPAWNAI_HELP)] 158 | [AutoComplete(Lang.SPAWNAI_ARGS)] 159 | private static void CCSpawnAI(ConCommandArgs args) 160 | { 161 | if (!Run.instance) 162 | { 163 | Log.MessageNetworked(Lang.NOTINARUN_ERROR, args, LogLevel.MessageClientOnly); 164 | return; 165 | } 166 | if (args.sender == null) 167 | { 168 | Log.Message(Lang.DS_NOTYETIMPLEMENTED, LogLevel.Error); 169 | return; 170 | } 171 | if (args.Count == 0) 172 | { 173 | Log.MessageNetworked(Lang.INSUFFICIENT_ARGS + Lang.SPAWNAI_ARGS, args, LogLevel.MessageClientOnly); 174 | return; 175 | } 176 | if (!args.senderBody) 177 | { 178 | Log.MessageNetworked("Can't spawn an object with relation to a dead target.", args, LogLevel.MessageClientOnly); 179 | return; 180 | } 181 | 182 | var masterIndex = StringFinder.Instance.GetAiFromPartial(args[0]); 183 | if (masterIndex == MasterCatalog.MasterIndex.none) 184 | { 185 | Log.MessageNetworked(Lang.SPAWN_ERROR + args[0], args, LogLevel.MessageClientOnly); 186 | return; 187 | } 188 | var masterPrefab = MasterCatalog.GetMasterPrefab(masterIndex); 189 | 190 | int amount = 1; 191 | if (args.Count > 1 && args[1] != Lang.DEFAULT_VALUE && !TextSerialization.TryParseInvariant(args[1], out amount)) 192 | { 193 | Log.MessageNetworked(String.Format(Lang.PARSE_ERROR, "count", "int"), args, LogLevel.MessageClientOnly); 194 | return; 195 | } 196 | 197 | EliteDef eliteDef = null; 198 | if (args.Count > 2 && args[2] != Lang.DEFAULT_VALUE) 199 | { 200 | var eliteIndex = StringFinder.Instance.GetEliteFromPartial(args[2]); 201 | if (eliteIndex == StringFinder.EliteIndex_NotFound) 202 | { 203 | Log.MessageNetworked(Lang.ELITE_NOTFOUND, args, LogLevel.MessageClientOnly); 204 | return; 205 | } 206 | eliteDef = EliteCatalog.GetEliteDef(eliteIndex); 207 | if (eliteDef && eliteDef.eliteEquipmentDef && Run.instance.IsEquipmentExpansionLocked(eliteDef.eliteEquipmentDef.equipmentIndex)) 208 | { 209 | var expansion = Util.GetExpansion(eliteDef.eliteEquipmentDef.requiredExpansion); 210 | Log.MessageNetworked(string.Format(Lang.EXPANSION_LOCKED, "elite equipment", expansion), args, LogLevel.WarningClientOnly); 211 | } 212 | } 213 | 214 | bool braindead = false; 215 | if (args.Count > 3 && args[3] != Lang.DEFAULT_VALUE && !Util.TryParseBool(args[3], out braindead)) 216 | { 217 | Log.MessageNetworked(String.Format(Lang.PARSE_ERROR, "braindead", "int or bool"), args, LogLevel.MessageClientOnly); 218 | return; 219 | } 220 | 221 | TeamIndex teamIndex = TeamIndex.Monster; 222 | if (args.Count > 4 && args[4] != Lang.DEFAULT_VALUE) 223 | { 224 | teamIndex = StringFinder.Instance.GetTeamFromPartial(args[4]); 225 | if (teamIndex == StringFinder.TeamIndex_NotFound) 226 | { 227 | Log.MessageNetworked(Lang.TEAM_NOTFOUND, args, LogLevel.MessageClientOnly); 228 | return; 229 | } 230 | } 231 | 232 | var spawnCard = StringFinder.Instance.GetDirectorCardFromPartial(masterPrefab.name)?.spawnCard; 233 | if (spawnCard == null) 234 | { 235 | spawnCard = ScriptableObject.CreateInstance<CharacterSpawnCard>(); 236 | spawnCard.prefab = masterPrefab; 237 | spawnCard.sendOverNetwork = true; 238 | var body = spawnCard.prefab.GetComponent<CharacterMaster>().bodyPrefab; 239 | if (body) 240 | { 241 | spawnCard.nodeGraphType = (body.GetComponent<CharacterMotor>() == null 242 | && (body.GetComponent<RigidbodyMotor>() != null || body.GetComponent<KinematicCharacterMotor>())) 243 | ? MapNodeGroup.GraphType.Air 244 | : MapNodeGroup.GraphType.Ground; 245 | } 246 | } 247 | var spawnRequest = new DirectorSpawnRequest( 248 | spawnCard, 249 | new DirectorPlacementRule 250 | { 251 | placementMode = DirectorPlacementRule.PlacementMode.Direct, 252 | position = args.senderBody.footPosition 253 | }, 254 | RoR2Application.rng 255 | ); 256 | spawnRequest.teamIndexOverride = teamIndex; 257 | spawnRequest.ignoreTeamMemberLimit = true; 258 | 259 | // The size of the monster's radius is required so multiple enemies do not spawn on the same spot. 260 | // This prevents the player from clipping into the ground, or flyers flinging themselves away. 261 | var radius = 1f; 262 | var prefab = spawnCard.prefab.GetComponent<CharacterMaster>().bodyPrefab; 263 | if (prefab) 264 | { 265 | var capsule = prefab.GetComponent<CapsuleCollider>(); 266 | if (capsule) 267 | { 268 | radius = capsule.radius; 269 | } 270 | else 271 | { 272 | var sphere = prefab.GetComponent<SphereCollider>(); 273 | if (sphere) 274 | { 275 | radius = sphere.radius; 276 | } 277 | } 278 | } 279 | // Just a hack for the Grandparent which still causes clipping otherwise 280 | if (prefab.name.Equals("GrandParentBody")) 281 | { 282 | radius = 0f; 283 | } 284 | 285 | var position = args.senderBody.footPosition + args.senderBody.transform.forward * (args.senderBody.radius + radius); 286 | var isFlyer = spawnCard.nodeGraphType == MapNodeGroup.GraphType.Air; 287 | if (isFlyer) 288 | { 289 | position = args.senderBody.transform.position; 290 | if (args.senderBody.characterMotor) 291 | { 292 | position.y += 0.5f * args.senderBody.characterMotor.capsuleHeight + 2f; 293 | } 294 | radius *= Mathf.Max(1f, 0.5f * amount); 295 | } 296 | 297 | Log.MessageNetworked(string.Format(Lang.SPAWN_ATTEMPT_2, amount, masterPrefab.name), args); 298 | for (int i = 0; i < amount; i++) 299 | { 300 | var spawnPosition = position; 301 | if (isFlyer) 302 | { 303 | var direction = Quaternion.AngleAxis(360f * ((float)i / amount), args.senderBody.transform.up) * args.senderBody.transform.forward; 304 | spawnPosition = position + (direction * radius); 305 | } 306 | var masterGameObject = spawnCard.DoSpawn(spawnPosition, Quaternion.identity, spawnRequest).spawnedInstance; 307 | if (masterGameObject) 308 | { 309 | CharacterMaster master = masterGameObject.GetComponent<CharacterMaster>(); 310 | if (eliteDef) 311 | { 312 | master.inventory.SetEquipmentIndex(eliteDef.eliteEquipmentDef.equipmentIndex); 313 | master.inventory.GiveItem(RoR2Content.Items.BoostHp, Mathf.RoundToInt((eliteDef.healthBoostCoefficient - 1) * 10)); 314 | master.inventory.GiveItem(RoR2Content.Items.BoostDamage, Mathf.RoundToInt(eliteDef.damageBoostCoefficient - 1) * 10); 315 | } 316 | if (braindead) 317 | { 318 | foreach (var ai in master.aiComponents) 319 | { 320 | UnityEngine.Object.Destroy(ai); 321 | } 322 | master.aiComponents = Array.Empty<BaseAI>(); 323 | } 324 | } 325 | } 326 | } 327 | 328 | [ConCommand(commandName = "spawn_body", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.SPAWNBODY_HELP)] 329 | [AutoComplete(Lang.SPAWNBODY_ARGS)] 330 | private static void CCSpawnBody(ConCommandArgs args) 331 | { 332 | if (!Run.instance) 333 | { 334 | Log.MessageNetworked(Lang.NOTINARUN_ERROR, args, LogLevel.MessageClientOnly); 335 | return; 336 | } 337 | if (args.sender == null) 338 | { 339 | Log.Message(Lang.DS_NOTYETIMPLEMENTED, LogLevel.Error); 340 | return; 341 | } 342 | if (args.Count == 0) 343 | { 344 | Log.MessageNetworked(Lang.INSUFFICIENT_ARGS + Lang.SPAWNBODY_ARGS, args, LogLevel.MessageClientOnly); 345 | return; 346 | } 347 | if (!args.senderBody) 348 | { 349 | Log.MessageNetworked("Can't spawn an object with relation to a dead target.", args, LogLevel.MessageClientOnly); 350 | return; 351 | } 352 | 353 | var bodyIndex = StringFinder.Instance.GetBodyFromPartial(args[0]); 354 | if (bodyIndex == BodyIndex.None) 355 | { 356 | Log.MessageNetworked(string.Format(Lang.SPAWN_ERROR, args[0]), args, LogLevel.MessageClientOnly); 357 | return; 358 | } 359 | 360 | GameObject body = BodyCatalog.GetBodyPrefab(bodyIndex); 361 | GameObject gameObject = UnityEngine.Object.Instantiate<GameObject>(body, args.senderBody.transform.position, Quaternion.identity); 362 | NetworkServer.Spawn(gameObject); 363 | Log.MessageNetworked(string.Format(Lang.SPAWN_ATTEMPT_1, body.name), args); 364 | } 365 | 366 | internal static void InitPortals() 367 | { 368 | portals.Add("artifact", Addressables.LoadAssetAsync<GameObject>("RoR2/Base/PortalArtifactworld/PortalArtifactworld.prefab").WaitForCompletion()); 369 | portals.Add("blue", Addressables.LoadAssetAsync<GameObject>("RoR2/Base/PortalShop/PortalShop.prefab").WaitForCompletion()); 370 | portals.Add("celestial", Addressables.LoadAssetAsync<GameObject>("RoR2/Base/PortalMS/PortalMS.prefab").WaitForCompletion()); 371 | portals.Add("deepvoid", Addressables.LoadAssetAsync<GameObject>("RoR2/DLC1/DeepVoidPortal/DeepVoidPortal.prefab").WaitForCompletion()); 372 | portals.Add("gold", Addressables.LoadAssetAsync<GameObject>("RoR2/Base/PortalGoldshores/PortalGoldshores.prefab").WaitForCompletion()); 373 | portals.Add("green", Addressables.LoadAssetAsync<GameObject>("RoR2/DLC2/PortalColossus.prefab").WaitForCompletion()); 374 | portals.Add("null", Addressables.LoadAssetAsync<GameObject>("RoR2/Base/PortalArena/PortalArena.prefab").WaitForCompletion()); 375 | portals.Add("void", Addressables.LoadAssetAsync<GameObject>("RoR2/DLC1/PortalVoid/PortalVoid.prefab").WaitForCompletion()); 376 | } 377 | 378 | internal static CombatDirector.EliteTierDef GetTierDef(EliteDef eliteDef) 379 | { 380 | if (!eliteDef) 381 | { 382 | return CombatDirector.eliteTiers[0]; 383 | } 384 | foreach (var eliteTier in CombatDirector.eliteTiers) 385 | { 386 | if (eliteTier != null) 387 | { 388 | foreach (var thisEliteDef in eliteTier.eliteTypes) 389 | { 390 | if (thisEliteDef) 391 | { 392 | if (thisEliteDef == eliteDef) 393 | { 394 | return eliteTier; 395 | } 396 | } 397 | } 398 | } 399 | } 400 | 401 | return CombatDirector.eliteTiers[0]; 402 | } 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /Code/DebugToolkit.cs: -------------------------------------------------------------------------------- 1 | using BepInEx; 2 | using BepInEx.Configuration; 3 | using DebugToolkit.Code; 4 | using DebugToolkit.Commands; 5 | using DebugToolkit.Permissions; 6 | using R2API.Utils; 7 | using RoR2; 8 | using System.Linq; 9 | using LogLevel = DebugToolkit.Log.LogLevel; 10 | 11 | namespace DebugToolkit 12 | { 13 | [BepInDependency(R2API.Networking.NetworkingAPI.PluginGUID)] 14 | [BepInDependency(R2API.PrefabAPI.PluginGUID)] 15 | [BepInDependency(R2API.ContentManagement.R2APIContentManager.PluginGUID)] 16 | [NetworkCompatibility(CompatibilityLevel.NoNeedForSync)] 17 | [BepInPlugin(GUID, modname, modver)] 18 | public class DebugToolkit : BaseUnityPlugin 19 | { 20 | public const string modname = "DebugToolkit", modver = "3.19.3"; 21 | public const string GUID = "iHarbHD." + modname; 22 | 23 | internal static ConfigFile Configuration; 24 | 25 | private void Awake() 26 | { 27 | Configuration = base.Config; 28 | 29 | new Log(Logger); 30 | 31 | //LogBuildInfo(); 32 | 33 | Log.Message("Created by Harb, iDeathHD and . Based on RoR2Cheats by Morris1927.", LogLevel.Info, Log.Target.Bepinex); 34 | 35 | MacroSystem.Init(); 36 | PermissionSystem.Init(); 37 | Hooks.InitializeHooks(); 38 | NetworkManager.Init(); 39 | } 40 | 41 | private void LogBuildInfo() 42 | { 43 | #region Not Release Message 44 | #if !RELEASE //Additional references in this block must be fully qualifed as to not use them in Release Builds. 45 | string gitVersion = ""; 46 | using (System.IO.Stream stream = System.Reflection.Assembly.GetExecutingAssembly() 47 | .GetManifestResourceStream($"{GetType().Namespace}.Resources.CurrentCommit")) 48 | using (System.IO.StreamReader reader = new System.IO.StreamReader(stream)) 49 | { 50 | gitVersion = reader.ReadToEnd(); 51 | } 52 | 53 | Log.MessageWarning( 54 | #if DEBUG 55 | $"This is a debug build!" 56 | #elif NONETWORK 57 | $"This is a non-networked build!" 58 | #elif BLEEDING 59 | $"This is a Bleeding-Edge build!" 60 | #endif 61 | , Log.Target.Bepinex); 62 | Log.MessageWarning($"Commit: {gitVersion.Trim()}", Log.Target.Bepinex); 63 | #endif 64 | #endregion 65 | } 66 | 67 | private void Start() 68 | { 69 | var _ = StringFinder.Instance; 70 | } 71 | 72 | private void Update() 73 | { 74 | MacroSystem.Update(); 75 | 76 | if (Run.instance && Command_Noclip.IsActivated) 77 | { 78 | Command_Noclip.Update(); 79 | } 80 | } 81 | 82 | public static void InvokeCMD(NetworkUser user, string commandName, params string[] arguments) 83 | { 84 | var args = arguments.ToList(); 85 | var consoleUser = new Console.CmdSender(user); 86 | if (Console.instance) 87 | Console.instance.RunCmd(consoleUser, commandName, args); 88 | else 89 | Log.Message("InvokeCMD called whilst no console instance exists.", LogLevel.Error, Log.Target.Bepinex); 90 | } 91 | 92 | 93 | /// <summary> 94 | /// Required for automated manifest building. 95 | /// </summary> 96 | /// <returns>Returns the TS manifest Version</returns> 97 | public static string GetModVer() 98 | { 99 | return modver; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Code/IgnoreAccessModifiers.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Permissions; 2 | 3 | // SecurityPermision set to minimum set to true 4 | // for skipping access modifiers check from the mono JIT 5 | // The same attribute is added to the assembly when ticking 6 | // Unsafe Code in the Project settings 7 | // This is done here to allow an explanation of the trick and 8 | // not in an outside source you could potentially miss. 9 | 10 | #pragma warning disable CS0618 // Type or member is obsolete 11 | [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] 12 | [assembly: HG.Reflection.SearchableAttribute.OptIn] 13 | #pragma warning restore CS0618 // Type or member is obsolete -------------------------------------------------------------------------------- /Code/Lang.cs: -------------------------------------------------------------------------------- 1 | namespace DebugToolkit 2 | { 3 | public static class Lang 4 | { 5 | // `{}` denotes necessary, `()` a list of specific choices, `[]` optional, `:` denotes a default 6 | 7 | // Command arguments 8 | public const string 9 | ADDPORTAL_ARGS = "Requires 1 argument: {portal ('blue'|'celestial'|'gold'|'green'|'void'|'all')}", 10 | BAN_ARGS = "Requires 1 argument: {player}", 11 | BIND_ARGS = "Requires 2 arguments: {key} [console_commands]", 12 | BIND_DELETE_ARGS = "Requires 1 argument: {key}", 13 | CHANGETEAM_ARGS = "Requires 1 (2 if from server) argument: {team} [player:<self>]", 14 | CHARGEZONE_ARGS = "Requires 1 argument: {charge}", 15 | CREATEPICKUP_ARGS = "Requires 1 (3 if from server) argument: {object (item|equip|'lunarcoin'|'voidcoin')} [search ('item'|'equip'|'both'):'both'] *[player:<self>]", 16 | CREATEPOTENTIAL_ARGS = "Requires 0 (3 if from server) arguments: [droptable (droptable|'all'):'all'] [count:3] *[player:<self>]", 17 | DUMPSTATE_ARGS = "Requires 0 (1 if from server) argument: [target (player|'pinged'):<self>]", 18 | DUMPSTATS_ARGS = "Requires 1 argument: {body}", 19 | FIXEDTIME_ARGS = "Requires 0 or 1 argument: [time]", 20 | FORCEWAVE_ARGS = "Requires 0 or 1 argument [wave_prefab]", 21 | GIVEBUFF_ARGS = "Requires 1 (4 if from server) arguments: {buff} [count:1] [duration:0] [target (player|'pinged'):<self>]", 22 | GIVEDOT_ARGS = "Requires 1 (4 if from server) argument: {dot} [count:1] [target (player|'pinged'):<self>] [attacker (player|'pinged'):<self>]", 23 | GIVEEQUIP_ARGS = "Requires 1 (2 if from server) argument: {equip (equip|'random')} [target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]", 24 | GIVEITEM_ARGS = "Requires 1 (3 if from server) argument: {item} [count:1] [target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]", 25 | GIVELUNAR_ARGS = "Requires 0 arguments: [amount:1]", 26 | GIVEMONEY_ARGS = "Requires 1 argument: {amount} [target (player|'all')]", 27 | HEAL_ARGS = "Requires 1 argument: {amount} [target (player|'pinged'):<self>]", 28 | HURT_ARGS = "Requires 1 argument: {amount} [target (player|'pinged'):<self>]", 29 | KICK_ARGS = "Requires 1 argument: {player}", 30 | KILLALL_ARGS = "Requires 0 arguments: [team:Monster]", 31 | LISTSKIN_ARGS = "Requires 0 arguments: [selection (body|'all'|'body'|'self'):'all']", 32 | LISTQUERY_ARGS = "Requires 0 arguments: [query]", 33 | LOADOUTSKIN_ARGS = "Requires 2 argument: {body_name (body|'self')} {skin_index}", 34 | NEXTBOSS_ARGS = "Requires 1 argument: {director_card} [count:1] [elite:None]", 35 | NEXTSTAGE_ARGS = "Requires 0 arguments: [specific_stage]", 36 | NO_ARGS = "Requires 0 arguments.", 37 | PERM_ENABLE_ARGS = "Requires 0 or 1 arguments: [value (0|1)]", 38 | PERM_MOD_ARGS = "Requires 2 arguments: {permission_level} {player}", 39 | POSTSOUNDEVENT_ARGS = "Requires 1 argument: {sound_event (event_name|event_id)}", 40 | PREVENT_PROFILE_WRITING_ARGS = "Requires 0 or 1 arguments [flag (0|1)]", 41 | RANDOMITEM_ARGS = "Requires 1 (3 if from server) argument: {count} [droptable (droptable|'all'):'all'] [target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]", 42 | REMOVEALLBUFFS_ARGS = "Requires 0 (2 if from server) arguments: [timed (0|1):0/false] [target (player|'pinged'):<self>]", 43 | REMOVEALLDOTS_ARGS = "Requires 0 (1 if from server) arguments: [target (player|'pinged'):<self>]", 44 | REMOVEALLITEMS_ARGS = "Requires 0 (1 if from server) arguments: [target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]", 45 | REMOVEBUFF_ARGS = "Requires 1 (4 if from server) arguments: {buff} [count:1] [timed (0|1):0/false] [target (player|'pinged'):<self>]", 46 | REMOVEBUFFSTACKS_ARGS = "Requires 1 (3 if from server) arguments: {buff} [timed (0|1):0/false] [target (player|'pinged'):<self>]", 47 | REMOVEDOT_ARGS = "Requires 1 (3 if from server) argument: {dot} [count:1] [target (player|'pinged'):<self>]", 48 | REMOVEDOTSTACKS_ARGS = "Requires 1 (2 if from server) argument: {dot} [target (player|'pinged'):<self>]", 49 | REMOVEEQUIP_ARGS = "Requires 0 (1 if from server) arguments: [target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]", 50 | REMOVEITEMSTACKS_ARGS = "Requires 1 (2 if from server) argument: {item} [target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]", 51 | RESPAWN_ARGS = "Requires 0 (1 if from server) arguments: [player:<self>]", 52 | RESTOCKEQUIP_ARGS = "Requires 0 (2 if from server) arguments: [count:1] *[target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]", 53 | RUNSETWAVESCLEARED_ARGS = "Requires 1 argument {wave}", 54 | SEED_ARGS = "Requires 0 or 1 argument: [new_seed]", 55 | SETARTIFACT_ARGS = "Requires 1 (2 if using 'all') argument: {artifact (artifact|'all')} [enable (0|1)]", 56 | SPAWNAI_ARGS = "Requires 1 argument: {ai} [count:1] [elite:None] [braindead (0|1):0/false] [team:Monster]", 57 | SPAWNAS_ARGS = "Requires 1 (2 if from server) argument: {body} [player:<self>]", 58 | SPAWNBODY_ARGS = "Requires 1 argument: {body}", 59 | SPAWNINTERACTABLE_ARGS = "Requires 1 argument: {interactable}", 60 | SPAWNPORTAL_ARGS = "Requires 1 argument: {portal ('artifact'|'blue'|'celestial'|'deepvoid'|'gold'|'green'|'null'|'void')}", 61 | TIMESCALE_ARGS = "Requires 1 argument: {time_scale}", 62 | TRUEKILL_ARGS = "Requires 0 (1 if from server) arguments: [player:<self>]" 63 | ; 64 | 65 | // Command help texts 66 | public const string 67 | ADDPORTAL_HELP = "Add a portal to the current Teleporter on completion. " + ADDPORTAL_ARGS, 68 | BAN_HELP = "Bans the specified player from the session. " + BAN_ARGS, 69 | BIND_HELP = "Bind a key to execute specific commands. " + BIND_ARGS, 70 | BIND_DELETE_HELP = "Remove a custom bind from the macro system of DebugToolkit. " + BIND_DELETE_ARGS, 71 | BIND_RELOAD_HELP = "Reload the macro system of DebugToolkit. " + NO_ARGS, 72 | BUDDHA_HELP = "Become immortal. Instead of refusing damage you just refuse to take lethal damage.\nIt works by giving all damage a player takes DamageType.NonLethal. " + NO_ARGS, 73 | CHANGETEAM_HELP = "Change the specified player to the specified team. " + CHANGETEAM_ARGS, 74 | CHARGEZONE_HELP = "Set the charge of all active holdout zones. " + CHARGEZONE_ARGS, 75 | CREATEPICKUP_HELP = "Creates a PickupDroplet infront of your position. " + CREATEPICKUP_ARGS, 76 | CREATEPOTENTIAL_HELP = "Creates a potential PickupDroplet infront of your position. " + CREATEPOTENTIAL_ARGS, 77 | CURSORTELEPORT_HELP = "Teleport you to where your cursor is currently aiming at. " + NO_ARGS, 78 | DUMPBUFFS_HELP = "List the buffs/debuffs of all spawned bodies. " + NO_ARGS, 79 | DUMPINVENTORIES_HELP = "List the inventory items and equipment of all spawned bodies. " + NO_ARGS, 80 | DUMPSTATE_HELP = "List the current stats, entity state, and skill cooldown of a specified body. " + DUMPSTATE_ARGS, 81 | DUMPSTATS_HELP = "List the base stats of a specific body. " + DUMPSTATS_ARGS, 82 | FAMILYEVENT_HELP = "Forces a family event to occur during the next stage. " + NO_ARGS, 83 | FIXEDTIME_HELP = "Sets the run timer to the specified value. " + FIXEDTIME_ARGS, 84 | FORCEWAVE_HELP = "Set the next wave prefab. Leave empty to see all options. " + FORCEWAVE_ARGS, 85 | GIVEBUFF_HELP = "Gives the specified buff to a character. A duration of 0 means permanent. " + GIVEBUFF_ARGS, 86 | GIVEDOT_HELP = "Gives the specified DoT to a character. " + GIVEDOT_ARGS, 87 | GIVEEQUIP_HELP = "Gives the specified equipment to a target. " + GIVEEQUIP_ARGS, 88 | GIVEITEM_HELP = "Gives the specified item to a target. " + GIVEITEM_ARGS, 89 | GIVELUNAR_HELP = "Gives a lunar coin to you. " + GIVELUNAR_ARGS, 90 | GIVEMONEY_HELP = "Gives the specified amount of money to the specified player. " + GIVEMONEY_ARGS, 91 | GOD_HELP = "Become invincible. " + NO_ARGS, 92 | HEAL_HELP = "Heal yourself or another target. " + HEAL_ARGS, 93 | HURT_HELP = "Deal generic damage to yourself or another target. " + HURT_ARGS, 94 | KICK_HELP = "Kicks the specified player from the session. " + KICK_ARGS, 95 | KILLALL_HELP = "Kill all entities on the specified team. " + KILLALL_ARGS, 96 | LISTAI_HELP = "List all Masters and their language invariants. " + LISTQUERY_ARGS, 97 | LISTARTIFACT_HELP = "List all Artifacts and their language invariants. " + LISTQUERY_ARGS, 98 | LISTBODY_HELP = "List all Bodies and their language invariants. " + LISTQUERY_ARGS, 99 | LISTBUFF_HELP = "List all Buffs and whether they stack. " + LISTQUERY_ARGS, 100 | LISTDIRECTORCARDS_HELP = "List all Director Cards. " + LISTQUERY_ARGS, 101 | LISTDOT_HELP = "List all DoTs. " + LISTQUERY_ARGS, 102 | LISTELITE_HELP = "List all Elites and their language invariants. " + LISTQUERY_ARGS, 103 | LISTEQUIP_HELP = "List all equipment and their availability. " + LISTQUERY_ARGS, 104 | LISTINTERACTABLE_HELP = "Lists all interactables. " + LISTQUERY_ARGS, 105 | LISTITEMTIER_HELP = "List all item tiers. " + LISTQUERY_ARGS, 106 | LISTITEM_HELP = "List all items and their availability. " + LISTQUERY_ARGS, 107 | LISTPLAYER_HELP = "List all players and their ID. " + LISTQUERY_ARGS, 108 | LISTSCENE_HELP = "List all scenes and their language invariants. " + LISTQUERY_ARGS, 109 | LISTSKIN_HELP = "List all bodies with skins. " + LISTSKIN_ARGS, 110 | LISTSURVIVOR_HELP = "List all survivors and their body/ai names. " + LISTQUERY_ARGS, 111 | LISTTEAM_HELP = "List all Teams and their language invariants. " + LISTQUERY_ARGS, 112 | LOADOUTSKIN_HELP = "Change your loadout's skin. " + LOADOUTSKIN_ARGS, 113 | LOCKEXP_HELP = "Toggle Experience gain. " + NO_ARGS, 114 | MACRO_DTZOOM_HELP = "Gives you 20 hooves and 200 feathers for getting around quickly.", 115 | MACRO_LATEGAME_HELP = "Sets the current run to the 'lategame' as defined by HG. This command is DESTRUCTIVE.", 116 | MACRO_MIDGAME_HELP = "Sets the current run to the 'midgame' as defined by HG. This command is DESTRUCTIVE.", 117 | NEXTBOSS_HELP = "Sets the next teleporter/simulacrum boss to the specified boss. " + NEXTBOSS_ARGS, 118 | NEXTSTAGE_HELP = "Forces a stage change to the specified stage. " + NEXTSTAGE_ARGS, 119 | NEXTWAVE_HELP = "Advance to the next Simulacrum wave. " + NO_ARGS, 120 | NOCLIP_HELP = "Allow flying and going through objects. Sprinting will double the speed. " + NO_ARGS, 121 | NOENEMIES_HELP = "Toggle Monster spawning. " + NO_ARGS, 122 | PERM_ENABLE_HELP = "Enable or disable the permission system." + PERM_ENABLE_ARGS, 123 | PERM_MOD_HELP = "Change the permission level of the specified playerid/username" + PERM_MOD_ARGS, 124 | PERM_RELOAD_HELP = "Reload the permission system, updates user and commands permissions.", 125 | POSTSOUNDEVENT_HELP = "Post a sound event to the AkSoundEngine (WWise) either by its event name or event ID. " + POSTSOUNDEVENT_ARGS, 126 | PREVENT_PROFILE_WRITING_HELP = "Prevent saving the user profile to avoid bogus data. " + PREVENT_PROFILE_WRITING_ARGS, 127 | RANDOMITEM_HELP = "Generate random items from the available item tiers. " + RANDOMITEM_ARGS, 128 | RELOADCONFIG_HELP = "Reload all default config files from all loaded plugins.", 129 | REMOVEALLBUFFS_HELP = "Removes all buffs from a character. " + REMOVEALLBUFFS_ARGS, 130 | REMOVEALLDOTS_HELP = "Removes all DoTs from a character. " + REMOVEALLDOTS_ARGS, 131 | REMOVEALLITEMS_HELP = "Removes all items from a target. " + REMOVEALLITEMS_ARGS, 132 | REMOVEBUFF_HELP = "Removes a specified buff from a character. Timed buffs prioritise the longest expiration stack. " + REMOVEBUFF_ARGS, 133 | REMOVEBUFFSTACKS_HELP = "Removes all stacks of a specified buff from a character. " + REMOVEBUFFSTACKS_ARGS, 134 | REMOVEDOT_HELP = "Remove a DoT stack with the longest expiration from a character. " + REMOVEDOT_ARGS, 135 | REMOVEDOTSTACKS_HELP = "Remove all stacks of a specified DoT from a character. " + REMOVEDOTSTACKS_ARGS, 136 | REMOVEEQUIP_HELP = "Removes the equipment from a target. " + REMOVEEQUIP_ARGS, 137 | REMOVEITEM_HELP = "Removes the specified quantities of an item from a target. " + GIVEITEM_ARGS, 138 | REMOVEITEMSTACKS_HELP = "Removes all the stacks of a specified item from a target. " + REMOVEITEMSTACKS_ARGS, 139 | RESPAWN_HELP = "Respawns the specified player. " + RESPAWN_ARGS, 140 | RESTOCKEQUIP_HELP = "Restock charges for the current equipment. " + RESTOCKEQUIP_ARGS, 141 | RUNSETWAVESCLEARED_HELP = "Set the Simulacrum waves cleared. Must be positive. " + RUNSETWAVESCLEARED_ARGS, 142 | SEED_HELP = "Gets/Sets the game seed until game close. Use 0 to reset to vanilla generation. " + SEED_ARGS, 143 | SETARTIFACT_HELP = "Enable/disable an Artifact. " + SETARTIFACT_ARGS, 144 | SPAWNAS_HELP = "Respawn the specified player using the specified body prefab. " + SPAWNAS_ARGS, 145 | SPAWNAI_HELP = "Spawns the specified CharacterMaster. " + SPAWNAI_ARGS, 146 | SPAWNBODY_HELP = "Spawns the specified dummy body. " + SPAWNBODY_ARGS, 147 | SPAWNINTERACTABLE_HELP = "Spawns the specified interactable. List_Interactable for options. " + SPAWNINTERACTABLE_ARGS, 148 | SPAWNPORTAL_HELP = "Spawns a portal in front of the player. " + SPAWNPORTAL_ARGS, 149 | TIMESCALE_HELP = "Sets the Time Delta. " + TIMESCALE_ARGS, 150 | TRUEKILL_HELP = "Ignore Dio's and kill the entity. " + TRUEKILL_ARGS 151 | ; 152 | 153 | // Messages 154 | public const string 155 | CREATEPICKUP_AMBIGIOUS_2 = "Could not choose between {0} and {1}, please be more precise. Consider using 'equip' or 'item' as the second argument.", 156 | CREATEPICKUP_NOTFOUND = "Could not find any item nor equipment with that name.", 157 | CREATEPICKUP_SUCCESS_1 = "Successfully created the pickup {0}.", 158 | CREATEPICKUP_SUCCESS_2 = "Successfully created a potential with {0} options.", 159 | GIVELUNAR_2 = "{0} {1} lunar coin(s).", 160 | GIVEOBJECT = "Gave {0} {1} to {2}.", 161 | OBSOLETEWARNING = "This command has become obsolete and will be removed in the next version. ", 162 | NETWORKING_OTHERPLAYER_4 = "{0}({1}) issued: {2} {3}", 163 | NOMESSAGE = "Yell at the modmakers if you see this message!", 164 | PARTIALIMPLEMENTATION_WARNING = "WARNING: PARTIAL IMPLEMENTATION. WIP.", 165 | PLAYER_DEADRESPAWN = "Player will spawn as the specified body next round. " + USE_RESPAWN, 166 | PLAYER_SKINCHANGERESPAWN = "Player will spawn with the specified skin next round. " + USE_RESPAWN, 167 | REMOVEOBJECT = "Removed {0} {1} from {2}.", 168 | RUNSETSTAGESCLEARED_HELP = "Sets the amount of stages cleared. This does not change the current stage.", 169 | SETTING_ENABLED = "{0} <color=green>enabled</color>", 170 | SETTING_DISABLED = "{0} <color=red>disabled</color>", 171 | SPAWN_ATTEMPT_1 = "Attempting to spawn: {0}", 172 | SPAWN_ATTEMPT_2 = "Attempting to spawn {0}: {1}", 173 | USE_RESPAWN = "Use 'respawn' to skip the wait." 174 | ; 175 | 176 | // Errors 177 | public const string 178 | BODY_NOTFOUND = "Specified body not found. Please use list_body for options.", 179 | DOTCONTROLLER_NOTFOUND = "The selected target has no DoTs.", 180 | ELITE_NOTFOUND = "Elite type not recognized. Please use list_elite for options.", 181 | EXPANSION_LOCKED = "An expansion is required for this {0}: {1}", 182 | INSUFFICIENT_ARGS = "Insufficient number of arguments. ", 183 | INTERACTABLE_NOTFOUND = "Interactable not found. Please use list_interactables for options.", 184 | INVALID_ARG_VALUE = "Invalid value for {0}.", 185 | INVENTORY_ERROR = "The selected target has no inventory.", 186 | NEGATIVE_ARG = "Negative value for {0} encountered. It needs to be zero or higher.", 187 | NOCONNECTION_ERROR = "Not connected to a server.", 188 | NOMATCH_ERROR = "No {0} found that match \"{1}\".", 189 | NOTINARUN_ERROR = "This command only works when in a Run!", 190 | NOTINASIMULACRUMRUN_ERROR = "Must be in a Simulacrum Run!", 191 | NOTINVOIDFIELDS_ERROR = "Must be on Void Fields!", 192 | OBJECT_NOTFOUND = "No {0} with the name \"{1}\" could be found.", 193 | PARSE_ERROR = "Unable to parse {0} to {1}.", 194 | PINGEDBODY_NOTFOUND = "Pinged target not found. Either the last ping was not a character, or it has been destroyed since.", 195 | PLAYER_NOTFOUND = "Specified player not found or isn't alive. Please use list_player for options.", 196 | SPAWN_ERROR = "Could not spawn: ", 197 | STAGE_NOTFOUND = "Stage not found. Please use list_scene for options.", 198 | TEAM_NOTFOUND = "Team type not found. Please use list_team for options." 199 | ; 200 | 201 | // Keywords 202 | public const string 203 | ALL = "ALL", 204 | BOTH = "BOTH", 205 | COIN_LUNAR = "LUNARCOIN", 206 | COIN_VOID = "VOIDCOIN", 207 | DEFAULT_VALUE = "", 208 | DEVOTION = "DEVOTION", 209 | EQUIP = "EQUIP", 210 | EVOLUTION = "EVOLUTION", 211 | ITEM = "ITEM", 212 | PINGED = "PINGED", 213 | RANDOM = "RANDOM", 214 | SIMULACRUM = "SIMULACRUM", 215 | VOIDFIELDS = "VOIDFIELDS" 216 | ; 217 | 218 | // Permissions 219 | public const string 220 | PS_ARGUSER_HAS_MORE_PERM = "Specified user {0} has a greater permission level than you.", 221 | PS_ARGUSER_HAS_SAME_PERM = "Specified user {0} has the same permission level as you.", 222 | PS_NO_REQUIRED_LEVEL = "You don't have the required permission {0} to use this command." 223 | ; 224 | 225 | // Dedicated server 226 | public const string 227 | DS_INVALIDARG = "This command doesn't support the '{0}' argument from a dedicated server.", 228 | DS_NOTAVAILABLE = "This command doesn't make sense to run from a dedicated server.", 229 | DS_NOTYETIMPLEMENTED = "This command has not yet been implemented to be run from a dedicated server," 230 | ; 231 | 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /Code/Log.cs: -------------------------------------------------------------------------------- 1 | using BepInEx.Logging; 2 | using RoR2; 3 | using RoR2.ConVar; 4 | using System.Reflection; 5 | using UnityEngine; 6 | using UnityEngine.Networking; 7 | 8 | namespace DebugToolkit 9 | { 10 | /* This class may be re-used and modified without attribution, as long as this notice remains.*/ 11 | 12 | internal class Log 13 | { 14 | private const bool BepinexInfoAlwaysLogs = true; 15 | private const int NetworkEnum = 69; 16 | 17 | private static ManualLogSource logger; 18 | 19 | /** <summary>Unless added to the game and modified by the user, this convar is equivalent to #if DEBUG</summary> 20 | */ 21 | public static BoolConVar DebugConvar = new BoolConVar 22 | ( 23 | $"{DebugToolkit.modname.ToLower()}_debug", 24 | RoR2.ConVarFlags.None, 25 | #if DEBUG 26 | "1", 27 | #else 28 | "0", 29 | #endif 30 | $"{DebugToolkit.modname} extensive debugging"); 31 | 32 | public Log(ManualLogSource bepLogger) 33 | { 34 | logger = bepLogger; 35 | } 36 | 37 | internal static void InitRPC() 38 | { 39 | NetworkManager.DebugToolKitComponents.AddComponent<LogNet>(); 40 | } 41 | 42 | /** <summary>Sends a message to a console.</summary> 43 | * <param name="input">The message to display</param> 44 | * <param name="level">The level of the message, note that info may always be displayed in some cases</param> 45 | * <param name="target">Target console, note that everything to ror2 is also passed to bepinex.</param> 46 | */ 47 | public static void Message(object input, LogLevel level = LogLevel.Message, Target target = Target.Ror2) 48 | { 49 | switch (target) 50 | { 51 | case Target.Ror2: 52 | Ror2Log(input, level); 53 | break; 54 | case Target.Bepinex: 55 | BepinexLog(input, level); 56 | break; 57 | default: 58 | int targetNr = (int)target; 59 | if (NetworkUser.readOnlyInstancesList.Count - 1 >= targetNr && targetNr >= 0) 60 | { 61 | if (input.GetType() != typeof(string)) 62 | { 63 | Message($"Couldn't send network message because the message was not a string: {input}.", LogLevel.Error, Target.Bepinex); 64 | return; 65 | } 66 | NetworkUser user = NetworkUser.readOnlyInstancesList[targetNr]; 67 | MessageInfo($"Send a network message to {targetNr}, length={((string)input).Length}"); 68 | Message((string)input, user, level); 69 | } 70 | else 71 | { 72 | Message($"Couldn't find target {targetNr} for message: {input}", LogLevel.Error, Target.Bepinex); 73 | } 74 | break; 75 | } 76 | } 77 | 78 | /** <summary>Sends a message back to the client that issued the command.</summary> 79 | * <param name="input">A string for the input, since we sending it over the network, we can't use arbitrary types.</param> 80 | * <param name="args">the commandargs</param> 81 | * <param name="level">The level to send the message at.</param> 82 | */ 83 | public static void MessageNetworked(string input, ConCommandArgs args, LogLevel level = LogLevel.Message) 84 | { 85 | if (args.sender != null && !args.sender.isLocalPlayer) 86 | { 87 | Message(input, args.sender, level); 88 | } 89 | if ((int)level < NetworkEnum || args.sender == null || args.sender.isLocalPlayer) 90 | { 91 | Message(input, level); 92 | } 93 | 94 | } 95 | 96 | 97 | /** <summary></summary> 98 | * <param name="input">The string to send</param> 99 | * <param name="networkUser">The user to target, may not be null</param> 100 | * <param name="level">The level, defaults to LogLevel.Message</param> 101 | * */ 102 | public static void Message(string input, NetworkUser networkUser, LogLevel level = LogLevel.Message) 103 | { 104 | if (networkUser == null) 105 | { 106 | return; 107 | } 108 | 109 | LogNet.Invoke(networkUser, input, (int)level); 110 | } 111 | 112 | /** <summary>Sends a warning to a console.</summary> 113 | * <param name="input">The message to display</param> 114 | * <param name="target">Target console, note that everything to ror2 is also passed to bepinex.</param> 115 | */ 116 | public static void MessageWarning(object input, Target target = Target.Ror2) 117 | { 118 | Message(input, LogLevel.Warning, target); 119 | } 120 | 121 | /** <summary>Sends info to a console, note that it may be surpressed in certain targets under certain conditions.</summary> 122 | * <param name="input">The message to display</param> 123 | * <param name="target">Target console, note that everything to ror2 is also passed to bepinex.</param> 124 | */ 125 | public static void MessageInfo(object input, Target target = Target.Ror2) 126 | { 127 | Message(input, LogLevel.Info, target); 128 | } 129 | 130 | private static void Ror2Log(object input, LogLevel level) 131 | { 132 | switch (level) 133 | { 134 | case LogLevel.Info: 135 | case LogLevel.InfoClientOnly: 136 | if (DebugConvar.value) 137 | { 138 | Debug.Log(input); 139 | } 140 | break; 141 | case LogLevel.Message: 142 | case LogLevel.MessageClientOnly: 143 | Debug.Log(input); 144 | break; 145 | case LogLevel.Warning: 146 | case LogLevel.WarningClientOnly: 147 | Debug.LogWarning(input); 148 | break; 149 | case LogLevel.Error: 150 | case LogLevel.ErrorClientOnly: 151 | Debug.LogError(input); 152 | break; 153 | } 154 | } 155 | 156 | private static void BepinexLog(object input, LogLevel level) 157 | { 158 | if (logger == null) 159 | { 160 | throw new System.NullReferenceException("Log Class in " + Assembly.GetExecutingAssembly().GetName().Name + " not initialized prior to message call!"); 161 | } 162 | switch (level) 163 | { 164 | case LogLevel.Info: 165 | case LogLevel.InfoClientOnly: 166 | if (BepinexInfoAlwaysLogs || DebugConvar.value) 167 | { 168 | logger.LogInfo(input); 169 | } 170 | break; 171 | case LogLevel.Message: 172 | case LogLevel.MessageClientOnly: 173 | logger.LogMessage(input); 174 | break; 175 | case LogLevel.Warning: 176 | case LogLevel.WarningClientOnly: 177 | logger.LogWarning(input); 178 | break; 179 | case LogLevel.Error: 180 | case LogLevel.ErrorClientOnly: 181 | logger.LogError(input); 182 | break; 183 | 184 | 185 | } 186 | } 187 | 188 | public enum LogLevel 189 | { 190 | Info = 0, 191 | Message = 1, 192 | Warning = 2, 193 | Error = 3, 194 | InfoClientOnly = NetworkEnum + Info, 195 | MessageClientOnly = NetworkEnum + Message, 196 | WarningClientOnly = NetworkEnum + Warning, 197 | ErrorClientOnly = NetworkEnum + Error 198 | } 199 | 200 | public enum Target 201 | { 202 | Bepinex = -2, 203 | Ror2 = -1 204 | } 205 | } 206 | 207 | // ReSharper disable once ClassNeverInstantiated.Global 208 | // ReSharper disable once MemberCanBeMadeStatic.Local 209 | // ReSharper disable once UnusedParameter.Local 210 | // ReSharper disable once UnusedMember.Local 211 | internal class LogNet : NetworkBehaviour 212 | { 213 | private static LogNet _instance; 214 | 215 | private void Awake() 216 | { 217 | _instance = this; 218 | } 219 | 220 | internal static void Invoke(NetworkUser networkUser, string msg, int level) 221 | { 222 | _instance.TargetLog(networkUser.connectionToClient, msg, level); 223 | } 224 | 225 | [TargetRpc] 226 | private void TargetLog(NetworkConnection _, string msg, int level) 227 | { 228 | Log.Message(msg, (Log.LogLevel)level); 229 | Hooks.ScrollConsoleDown(); 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /Code/MacroSystem.cs: -------------------------------------------------------------------------------- 1 | using BepInEx.Configuration; 2 | using RoR2; 3 | using RoR2.UI; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text.RegularExpressions; 8 | using UnityEngine; 9 | 10 | namespace DebugToolkit.Code 11 | { 12 | internal static class MacroSystem 13 | { 14 | private static readonly string[] MODIFIERS = [ 15 | "left alt", 16 | "left ctrl", 17 | "left shift", 18 | "right alt", 19 | "right ctrl", 20 | "right shift" 21 | ]; 22 | 23 | private static readonly string MACRO_DTBIND_1 = "dt_bind {0} {1}"; 24 | private static readonly string MACRO_DTBIND_2 = "dt_bind ({0}) {1}"; 25 | 26 | private static (string[], string[], string[]) KeyBindParser(string keyBind) 27 | { 28 | // Split key combinations with +, but '+' and '[+]' can be legitimate keys 29 | var keys = Regex.Matches(keyBind, @"(.+?)(?:(?<!\[)\+(?!\])|$)").Select(x => x.Groups[1].Value).ToHashSet(); 30 | var modifiers = new List<string>(); 31 | var missingModifiers = new List<string>(); 32 | foreach (var modifier in MODIFIERS) 33 | { 34 | if (keys.Contains(modifier)) 35 | { 36 | modifiers.Add(modifier); 37 | keys.Remove(modifier); 38 | } 39 | else 40 | { 41 | missingModifiers.Add(modifier); 42 | } 43 | } 44 | return (modifiers.ToArray(), missingModifiers.ToArray(), keys.OrderBy(x => x).ToArray()); 45 | } 46 | 47 | private static string GetKeyBindInvariant(string keyBind) 48 | { 49 | (var modifiers, _, var keys) = KeyBindParser(keyBind); 50 | return GetKeyBindInvariant(modifiers, keys); 51 | } 52 | 53 | private static string GetKeyBindInvariant(string[] modifiers, string[] keys) 54 | { 55 | var keyBind = string.Join("+", keys); 56 | if (modifiers.Length > 0) 57 | { 58 | return $"{string.Join("+", modifiers)}+{keyBind}"; 59 | } 60 | return keyBind; 61 | } 62 | 63 | private class MacroConfigEntry 64 | { 65 | internal ConfigEntry<string> ConfigEntry { get; } 66 | private string[] BlobArray { get; } 67 | 68 | internal string KeyBind { get; private set; } 69 | internal string Key { get; private set; } 70 | internal string[] Modifiers { get; private set; } 71 | internal string[] MissingModifiers { get; private set; } 72 | internal string[] ConsoleCommands { get; set; } 73 | private string _consoleCommandsBlob; 74 | 75 | internal MacroConfigEntry(ConfigEntry<string> configEntry) 76 | { 77 | BlobArray = SplitBindCmd(configEntry.Value); 78 | ConfigEntry = configEntry; 79 | } 80 | 81 | private static string[] SplitBindCmd(string bindCmdBlob) 82 | { 83 | var match = Regex.Match(bindCmdBlob, @"dt_bind\s+(\((?<key>.+)\)\s+(?<command>.+)|(?<key>[^\s]+)\s+(?<command>.+))"); 84 | if (match.Success) 85 | { 86 | return [match.Groups["key"].Value, match.Groups["command"].Value]; 87 | } 88 | return []; 89 | } 90 | 91 | internal bool IsCorrectlyFormatted() 92 | { 93 | if (BlobArray.Length < 2) 94 | { 95 | Log.Message($"Missing parameters for macro config entry called {ConfigEntry.Definition.Key}.", Log.LogLevel.ErrorClientOnly); 96 | return false; 97 | } 98 | 99 | KeyBind = BlobArray[0]; 100 | _consoleCommandsBlob = BlobArray[1]; 101 | ConsoleCommands = SplitConsoleCommandsBlob(_consoleCommandsBlob); 102 | (Modifiers, MissingModifiers, var keys) = KeyBindParser(KeyBind); 103 | if (keys.Length == 0) 104 | { 105 | Log.Message($"No key was defined for the macro '{KeyBind}'.", Log.LogLevel.ErrorClientOnly); 106 | return false; 107 | } 108 | else if (keys.Length > 1) 109 | { 110 | Log.Message($"Multiple keys '{string.Join("+", keys)}' were defined for the macro '{KeyBind}'.", Log.LogLevel.ErrorClientOnly); 111 | return false; 112 | } 113 | Key = keys[0]; 114 | 115 | try 116 | { 117 | Input.GetKeyDown(Key); 118 | } 119 | catch (ArgumentException) 120 | { 121 | Log.Message($"Specified key '{Key}' for the macro '{KeyBind}' does not exist.", Log.LogLevel.ErrorClientOnly); 122 | return false; 123 | } 124 | 125 | var invariantKeyBind = GetKeyBindInvariant(Modifiers, [Key]); 126 | if (invariantKeyBind != KeyBind) 127 | { 128 | KeyBind = invariantKeyBind; 129 | ConfigEntry.Value = string.Format(KeyBind.Contains(" ") ? MACRO_DTBIND_2 : MACRO_DTBIND_1, KeyBind, _consoleCommandsBlob); 130 | } 131 | 132 | return true; 133 | } 134 | } 135 | 136 | private static readonly Dictionary<string, MacroConfigEntry> MacroConfigEntries = new Dictionary<string, MacroConfigEntry>(); 137 | 138 | private static readonly System.Reflection.PropertyInfo OrphanedEntriesProperty = 139 | typeof(ConfigFile).GetProperty("OrphanedEntries", 140 | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); 141 | 142 | private static Dictionary<ConfigDefinition, string> OrphanedEntries => 143 | (Dictionary<ConfigDefinition, string>)OrphanedEntriesProperty.GetValue(DebugToolkit.Configuration); 144 | 145 | private static ConfigEntry<bool> AllowKeybindsWithUi; 146 | 147 | private const string MACRO_SECTION_NAME = "Macros"; 148 | private const string DEFAULT_MACRO_NAME = "Do not remove this example macro"; 149 | private const string DEFAULT_MACRO_KEYBIND = "f15"; 150 | private const string DEFAULT_MACRO_VALUE = "dt_bind " + DEFAULT_MACRO_KEYBIND + " help"; 151 | private const string DEFAULT_MACRO_DESCRIPTION = "Custom keybind for executing console commands."; 152 | private const char DEFAULT_COMMAND_SEPARATOR = ';'; 153 | 154 | private const string MACRO_MINI_TUTORIAL = 155 | "\nMust start with dt_bind {KeyBind} {ConsoleCommands}.\n" + 156 | "Example : dt_bind x noclip;kill_all\n" + 157 | "When you'll press x key on keyboard it'll activate noclip and kill every monster.\n" + 158 | "For adding new macros, just add new lines under the example, must be formatted like this :\n" + 159 | "Macro 2 = dt_bind z no_enemies;give_item hoof 10\n" + 160 | "Macro 3 = dt_bind x give_item dagger 5;give_item syringe 10\n" + 161 | "Or use the in-game console and use the dt_bind console command.\n" + 162 | "When doing it from the in game console, don't forget to use double quotes, especially when chaining commands !\n" + 163 | "dt_bind b \"give_item dio 1;spawn_ai beetle 1\"\n" + 164 | "Binding a key whose name has a space also requires double quotes from the console:\n" + 165 | "dt_bind \"page down\" kill_all\n" + 166 | "While here it is formatted with brackets like this:\n" + 167 | "dt_bind (page down) kill_all\n" + 168 | "It is also possible to define a key combination with \"+\" and any combination of \"alt\", \"ctrl\", or \"shift\" as modifiers:\n" + 169 | "dt_bind \"left shift+r\" respawn\n" + 170 | "You can also delete existing bind like this:\n" + 171 | "dt_bind_delete {KeyBind}"; 172 | 173 | internal static void Init() 174 | { 175 | DebugToolkit.Configuration.Save(); 176 | Reload(); 177 | 178 | DebugToolkit.Configuration.SettingChanged += OnMacroSettingChanged; 179 | RoR2Application.onLoad += CollectInspectorObjects; 180 | } 181 | 182 | private static GameObject runtimeInspector; 183 | private static GameObject unityInspector; 184 | 185 | private static void CollectInspectorObjects() 186 | { 187 | var objs = Resources.FindObjectsOfTypeAll<Transform>(); 188 | foreach (var obj in objs) 189 | { 190 | if (obj.gameObject.name == "RuntimeInspectorCanvas") 191 | { 192 | runtimeInspector = obj.gameObject; 193 | } 194 | else if (obj.gameObject.name == "com.sinai.unityexplorer_Root") 195 | { 196 | unityInspector = obj.gameObject; 197 | } 198 | } 199 | } 200 | 201 | private static void OnMacroSettingChanged(object sender, SettingChangedEventArgs e) 202 | { 203 | if (e.ChangedSetting.Definition.Section == MACRO_SECTION_NAME) 204 | { 205 | Reload(); 206 | } 207 | } 208 | 209 | private static void Reload() 210 | { 211 | MacroConfigEntries.Clear(); 212 | DebugToolkit.Configuration.Reload(); 213 | 214 | BindExampleMacro(); 215 | 216 | BindExistingEntries(); 217 | RetrieveOrphanedMacroEntries(); 218 | 219 | BindSettings(); 220 | } 221 | 222 | private static void BindExampleMacro() 223 | { 224 | AddMacroFromConfigEntry(DebugToolkit.Configuration.Bind(MACRO_SECTION_NAME, DEFAULT_MACRO_NAME, 225 | DEFAULT_MACRO_VALUE, DEFAULT_MACRO_DESCRIPTION + MACRO_MINI_TUTORIAL)); 226 | } 227 | 228 | private static void BindExistingEntries() 229 | { 230 | foreach (var configDef in DebugToolkit.Configuration.Keys) 231 | { 232 | if (configDef.Section == MACRO_SECTION_NAME && configDef.Key != DEFAULT_MACRO_NAME) 233 | { 234 | AddMacroFromConfigEntry(DebugToolkit.Configuration.Bind(configDef, DEFAULT_MACRO_VALUE, new ConfigDescription(DEFAULT_MACRO_DESCRIPTION))); 235 | } 236 | } 237 | } 238 | 239 | private static void RetrieveOrphanedMacroEntries() 240 | { 241 | var orphanedEntries = OrphanedEntries; 242 | var newEntries = new List<(ConfigDefinition, string)>(); 243 | 244 | foreach (var (configDef, value) in orphanedEntries) 245 | { 246 | if (configDef.Section == MACRO_SECTION_NAME && !string.IsNullOrEmpty(value)) 247 | { 248 | newEntries.Add((configDef, value)); 249 | } 250 | } 251 | 252 | foreach (var (configDef, value) in newEntries) 253 | { 254 | orphanedEntries.Remove(configDef); 255 | 256 | AddMacroFromConfigEntry(BindNewConfigEntry(value)); 257 | } 258 | } 259 | 260 | private static void BindSettings() 261 | { 262 | AllowKeybindsWithUi = DebugToolkit.Configuration.Bind("Macros.Settings", "AllowKeybindsWithUi", true, 263 | "Allow keybinds to execute when a UI window is open, e.g., Console or an Inspector."); 264 | } 265 | 266 | private static ConfigEntry<string> BindNewConfigEntry(string customValue) 267 | { 268 | var macroAffix = "Macro "; 269 | var existingNumbers = new HashSet<int>(); 270 | foreach (var configDef in DebugToolkit.Configuration.Keys) 271 | { 272 | if (configDef.Section == MACRO_SECTION_NAME) 273 | { 274 | if (configDef.Key.StartsWith(macroAffix) && int.TryParse(configDef.Key.Substring(macroAffix.Length - 1), out var n)) 275 | { 276 | existingNumbers.Add(n); 277 | } 278 | } 279 | } 280 | var newIndex = 2; 281 | while (existingNumbers.Contains(newIndex)) 282 | { 283 | newIndex++; 284 | } 285 | 286 | var configEntry = DebugToolkit.Configuration.Bind(MACRO_SECTION_NAME, 287 | macroAffix + newIndex, DEFAULT_MACRO_VALUE, DEFAULT_MACRO_DESCRIPTION); 288 | configEntry.Value = customValue; 289 | 290 | return configEntry; 291 | } 292 | 293 | private static void AddMacroFromConfigEntry(ConfigEntry<string> macroEntry) 294 | { 295 | var macroConfigEntry = new MacroConfigEntry(macroEntry); 296 | if (macroConfigEntry.IsCorrectlyFormatted()) 297 | { 298 | MacroConfigEntries[macroConfigEntry.KeyBind] = macroConfigEntry; 299 | } 300 | } 301 | 302 | private static string[] SplitConsoleCommandsBlob(string consoleCommandsBlob) 303 | { 304 | return consoleCommandsBlob.Split(new[] { DEFAULT_COMMAND_SEPARATOR }, StringSplitOptions.RemoveEmptyEntries); 305 | } 306 | 307 | internal static void Update() 308 | { 309 | if (!IsAnyInputFieldActive() 310 | && (!PauseManager.pauseScreenInstance || !PauseManager.pauseScreenInstance.activeSelf) 311 | && (AllowKeybindsWithUi.Value || !IsAnyUIWindowOpen()) 312 | ) 313 | { 314 | // Iterating on a copy of the keys in case a macro executes a bind/delete and modifies the collection 315 | var keyBinds = new List<string>(MacroConfigEntries.Keys); 316 | foreach (var keyBind in keyBinds) 317 | { 318 | if (MacroConfigEntries.TryGetValue(keyBind, out var macroConfigEntry)) 319 | { 320 | if (Input.GetKeyDown(macroConfigEntry.Key) 321 | && macroConfigEntry.Modifiers.All(Input.GetKey) 322 | && !macroConfigEntry.MissingModifiers.Any(Input.GetKey)) 323 | { 324 | foreach (var consoleCommand in macroConfigEntry.ConsoleCommands) 325 | { 326 | RoR2.Console.instance.SubmitCmd(NetworkUser.readOnlyLocalPlayersList.FirstOrDefault(), consoleCommand); 327 | } 328 | } 329 | } 330 | } 331 | } 332 | } 333 | 334 | private static bool IsAnyUIWindowOpen() 335 | { 336 | return ConsoleWindow.instance 337 | || runtimeInspector && runtimeInspector.activeSelf 338 | || unityInspector && unityInspector.activeSelf; 339 | } 340 | 341 | private static bool IsAnyInputFieldActive() 342 | { 343 | return (MPEventSystem.instancesList != null && MPEventSystem.instancesList.Any(eventSystem => eventSystem && eventSystem.currentSelectedGameObject)) 344 | || (UnityEngine.EventSystems.EventSystem.current && UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject); 345 | } 346 | 347 | [ConCommand(commandName = "dt_bind", flags = ConVarFlags.None, helpText = Lang.BIND_HELP)] 348 | [AutoComplete(Lang.BIND_HELP)] 349 | private static void CCBindMacro(ConCommandArgs args) 350 | { 351 | if (args.Count >= 2) 352 | { 353 | BindExampleMacro(); 354 | 355 | // We only want 2 substrings. (the key bind, and the console commands) 356 | // We need to identify keys that can have spaces, e.g. "page down", since 357 | // the bind arguments are parsed from the config by space splitting. 358 | // Quotes seem to get messed up in the config file, so brackets it is. 359 | var bindCmdBlob = string.Format(args[0].Contains(" ") ? MACRO_DTBIND_2 : MACRO_DTBIND_1, args[0], args[1]); 360 | 361 | var keyBind = GetKeyBindInvariant(args[0]); 362 | var consoleCommandsBlob = args[1]; 363 | if (MacroConfigEntries.TryGetValue(keyBind, out var existingMacro)) 364 | { 365 | existingMacro.ConfigEntry.Value = bindCmdBlob; 366 | existingMacro.ConsoleCommands = SplitConsoleCommandsBlob(consoleCommandsBlob); 367 | } 368 | else 369 | { 370 | AddMacroFromConfigEntry(BindNewConfigEntry(bindCmdBlob)); 371 | } 372 | } 373 | else if (args.Count == 1) 374 | { 375 | Log.Message(MacroConfigEntries.TryGetValue(GetKeyBindInvariant(args[0]), out var existingMacro) 376 | ? existingMacro.ConfigEntry.Value 377 | : "This key has no macro associated to it."); 378 | } 379 | else 380 | { 381 | Log.Message(Lang.INSUFFICIENT_ARGS + Lang.BIND_ARGS, Log.LogLevel.ErrorClientOnly); 382 | } 383 | } 384 | 385 | [ConCommand(commandName = "dt_bind_delete", flags = ConVarFlags.None, helpText = Lang.BIND_DELETE_HELP)] 386 | [AutoComplete(Lang.BIND_DELETE_ARGS)] 387 | private static void CCBindDeleteMacro(ConCommandArgs args) 388 | { 389 | if (args.Count == 1) 390 | { 391 | var keyBind = args[0]; 392 | 393 | if (keyBind == DEFAULT_MACRO_KEYBIND) 394 | { 395 | Log.Message("You can't delete the default macro.", Log.LogLevel.ErrorClientOnly); 396 | return; 397 | } 398 | 399 | keyBind = GetKeyBindInvariant(keyBind); 400 | if (MacroConfigEntries.TryGetValue(keyBind, out var macroEntry)) 401 | { 402 | DebugToolkit.Configuration.Remove(macroEntry.ConfigEntry.Definition); 403 | DebugToolkit.Configuration.Save(); 404 | MacroConfigEntries.Remove(keyBind); 405 | } 406 | } 407 | else 408 | { 409 | Log.Message(Lang.INSUFFICIENT_ARGS + Lang.BIND_DELETE_ARGS, Log.LogLevel.ErrorClientOnly); 410 | } 411 | } 412 | 413 | [ConCommand(commandName = "dt_bind_reload", flags = ConVarFlags.None, helpText = Lang.BIND_RELOAD_HELP)] 414 | private static void CCBindReloadMacro(ConCommandArgs _) 415 | { 416 | Reload(); 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /Code/NetworkManager.cs: -------------------------------------------------------------------------------- 1 | using DebugToolkit.Commands; 2 | using R2API; 3 | using RoR2; 4 | using UnityEngine; 5 | using UnityEngine.Networking; 6 | using UnityObject = UnityEngine.Object; 7 | 8 | namespace DebugToolkit 9 | { 10 | internal static class NetworkManager 11 | { 12 | internal static GameObject DebugToolKitComponents; 13 | private static GameObject _debugToolKitComponentsSpawned; 14 | 15 | internal static void Init() 16 | { 17 | var dtcn = new GameObject("dtcn"); 18 | dtcn.AddComponent<NetworkIdentity>(); 19 | DebugToolKitComponents = dtcn.InstantiateClone("DebugToolKitComponentsNetworked"); 20 | UnityObject.Destroy(dtcn); 21 | 22 | Log.InitRPC(); 23 | Command_Noclip.InitRPC(); 24 | Command_Teleport.InitRPC(); 25 | DebugToolKitComponents.AddComponent<TimescaleNet>(); 26 | DebugToolKitComponents.AddComponent<SetDontDestroyOnLoad>(); 27 | } 28 | 29 | internal static void CreateNetworkObject(On.RoR2.NetworkSession.orig_Start orig, NetworkSession self) 30 | { 31 | orig(self); 32 | if (!_debugToolKitComponentsSpawned && NetworkServer.active) 33 | { 34 | _debugToolKitComponentsSpawned = UnityObject.Instantiate(DebugToolKitComponents); 35 | NetworkServer.Spawn(_debugToolKitComponentsSpawned); 36 | } 37 | } 38 | 39 | internal static void DestroyNetworkObject(On.RoR2.NetworkSession.orig_OnDestroy orig, NetworkSession self) 40 | { 41 | if (_debugToolKitComponentsSpawned && NetworkServer.active) 42 | { 43 | NetworkServer.Destroy(_debugToolKitComponentsSpawned); 44 | _debugToolKitComponentsSpawned = null; 45 | } 46 | orig(self); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Code/PermissionSystem.cs: -------------------------------------------------------------------------------- 1 | using BepInEx.Configuration; 2 | using RoR2; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace DebugToolkit.Permissions 9 | { 10 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 11 | public class RequiredLevel : Attribute 12 | { 13 | internal readonly Level Level; 14 | 15 | public RequiredLevel(Level level = Level.SubAdmin) 16 | { 17 | Level = level; 18 | } 19 | } 20 | 21 | public enum Level 22 | { 23 | None, 24 | SubAdmin, 25 | Admin 26 | } 27 | 28 | public static class PermissionSystem 29 | { 30 | internal static ConfigEntry<bool> IsEnabled; 31 | 32 | private static ConfigEntry<string> _adminList; 33 | private static ConfigEntry<string> _subAdminList; 34 | 35 | private static readonly HashSet<ulong> AdminSteamIdList = new HashSet<ulong>(); 36 | private static readonly HashSet<ulong> SubAdminSteamIdList = new HashSet<ulong>(); 37 | 38 | private static ConfigEntry<bool> _roR2CommandsNeedPermission; 39 | 40 | private static ConfigEntry<Level> _defaultPermissionLevel; 41 | private static readonly Dictionary<string, ConfigEntry<Level>> AdminCommands = new Dictionary<string, ConfigEntry<Level>>(); 42 | 43 | public const string PERMISSION_SYSTEM_CONFIG_SECTION = "Permission System"; 44 | public const char PERMISSION_SYSTEM_ADMIN_LIST_SEPARATOR = ','; 45 | public const string ADMIN_LIST_CONFIG_NAME = "2. Admin"; 46 | public const string SUBADMIN_LIST_CONFIG_NAME = "3. Sub Admin List"; 47 | 48 | internal static void Init() 49 | { 50 | IsEnabled = DebugToolkit.Configuration.Bind("Permission System", "1. Enable", false, 51 | "Is the Permission System enabled."); 52 | 53 | if (!IsEnabled.Value) 54 | return; 55 | 56 | _adminList = DebugToolkit.Configuration.Bind("Permission System", ADMIN_LIST_CONFIG_NAME, "76561197960265728, 76561197960265729", 57 | "Who is/are the admin(s). They are identified using their STEAM64 ID, the ID can be seen when the player connects to the server."); 58 | _subAdminList = DebugToolkit.Configuration.Bind("Permission System", SUBADMIN_LIST_CONFIG_NAME, "76561197960265730, 76561197960265731", 59 | "Who is/are the sub admin(s)."); 60 | 61 | _defaultPermissionLevel = DebugToolkit.Configuration.Bind("Permission System", "4. Default Permission Level", Level.SubAdmin, 62 | "What is the default permission level to use DebugToolkit commands, available levels : None (0), SubAdmin (1), Admin (2)"); 63 | 64 | _roR2CommandsNeedPermission = DebugToolkit.Configuration.Bind("Permission System", 65 | "5. RoR2 Console Commands use the Permission System", false, 66 | "When enabled, all the RoR2 Console Commands will be added to this config file and will only be fired if the permission check says so."); 67 | 68 | UpdateAdminListCache(); 69 | UpdateSubAdminListCache(); 70 | DebugToolkit.Configuration.SettingChanged += UpdateAdminListCacheOnConfigChange; 71 | 72 | AdminCommands.Clear(); 73 | 74 | AddPermissionCheckToConCommands(); 75 | 76 | if (_roR2CommandsNeedPermission.Value) 77 | AddPermissionCheckToConCommands(typeof(RoR2Application).Assembly); 78 | } 79 | 80 | private static void UpdateAdminListCache() 81 | { 82 | var adminSteamIds = _adminList.Value.Split(PERMISSION_SYSTEM_ADMIN_LIST_SEPARATOR).Select(s => s.Trim()).ToList(); 83 | 84 | AdminSteamIdList.Clear(); 85 | foreach (var steamId in adminSteamIds) 86 | { 87 | if (ulong.TryParse(steamId, out var steamIdULong)) 88 | { 89 | AdminSteamIdList.Add(steamIdULong); 90 | } 91 | else 92 | { 93 | Log.Message($"Can't parse correctly the given STEAMID64 : ${steamId} for admins", Log.LogLevel.Error); 94 | } 95 | } 96 | } 97 | 98 | private static void UpdateSubAdminListCache() 99 | { 100 | var subAdminSteamIds = _subAdminList.Value.Split(PERMISSION_SYSTEM_ADMIN_LIST_SEPARATOR).Select(s => s.Trim()).ToList(); 101 | 102 | SubAdminSteamIdList.Clear(); 103 | foreach (var steamId in subAdminSteamIds) 104 | { 105 | if (ulong.TryParse(steamId, out var steamIdULong)) 106 | { 107 | SubAdminSteamIdList.Add(steamIdULong); 108 | } 109 | else 110 | { 111 | Log.Message($"Can't parse correctly the given STEAMID64 : ${steamId} for sub admins", Log.LogLevel.Error); 112 | } 113 | } 114 | } 115 | 116 | private static void UpdateAdminListCacheOnConfigChange(object _, SettingChangedEventArgs settingChangedEventArgs) 117 | { 118 | var changedSetting = settingChangedEventArgs.ChangedSetting; 119 | 120 | if (changedSetting.Definition.Section == PERMISSION_SYSTEM_CONFIG_SECTION) 121 | { 122 | if (changedSetting.Definition.Key == ADMIN_LIST_CONFIG_NAME) 123 | { 124 | UpdateAdminListCache(); 125 | } 126 | else if (changedSetting.Definition.Key == SUBADMIN_LIST_CONFIG_NAME) 127 | { 128 | UpdateSubAdminListCache(); 129 | } 130 | } 131 | } 132 | 133 | // If no specific assembly is specified, 134 | // the assembly that calls this method will see their methods 135 | // being added to the permission system config file 136 | // and the permission will be checked if enabled. 137 | // 138 | // ReSharper disable once MemberCanBePrivate.Global 139 | public static void AddPermissionCheckToConCommands(Assembly assembly = null) 140 | { 141 | if (assembly == null) 142 | assembly = Assembly.GetCallingAssembly(); 143 | 144 | Type[] types; 145 | try 146 | { 147 | types = assembly.GetTypes(); 148 | } 149 | catch (ReflectionTypeLoadException ex) 150 | { 151 | types = ex.Types.Where(x => x != null).ToArray(); 152 | foreach (var e in ex.LoaderExceptions) 153 | { 154 | Log.Message(ex.Message, Log.LogLevel.Error, Log.Target.Bepinex); 155 | } 156 | } 157 | foreach (var methodInfo in types.SelectMany(x => x.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))) 158 | { 159 | object[] attributes; 160 | try 161 | { 162 | attributes = methodInfo.GetCustomAttributes(false); 163 | } 164 | catch (TypeLoadException ex) 165 | { 166 | Log.Message(ex.Message, Log.LogLevel.Error, Log.Target.Bepinex); 167 | continue; 168 | } 169 | catch (InvalidOperationException ex) 170 | { 171 | Log.Message(ex.Message, Log.LogLevel.Error, Log.Target.Bepinex); 172 | continue; 173 | } 174 | if (attributes == null) 175 | { 176 | continue; 177 | } 178 | var conCommandAttributes = attributes.OfType<ConCommandAttribute>().ToArray(); 179 | if (conCommandAttributes.Length > 0) 180 | { 181 | var adminCommandAttribute = methodInfo.GetCustomAttribute<RequiredLevel>(false); 182 | var usedPermissionLevel = adminCommandAttribute?.Level ?? _defaultPermissionLevel.Value; 183 | foreach (var conCommand in conCommandAttributes) 184 | { 185 | if ((conCommand.flags & ConVarFlags.ExecuteOnServer) == ConVarFlags.ExecuteOnServer) 186 | { 187 | if (conCommand.commandName == "say") 188 | { 189 | usedPermissionLevel = Level.None; 190 | } 191 | var overrideConfigEntry = DebugToolkit.Configuration.Bind("Permission System", $"Override: {conCommand.commandName}", usedPermissionLevel, 192 | $"Override Required Permission Level for the {conCommand.commandName} command"); 193 | 194 | AdminCommands.Add(conCommand.commandName, overrideConfigEntry); 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | [ConCommand(commandName = "perm_reload", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.PERM_RELOAD_HELP)] 202 | [RequiredLevel(Level.Admin)] 203 | private static void CCReloadPermissionSystem(ConCommandArgs args) 204 | { 205 | DebugToolkit.Configuration.Reload(); 206 | Init(); 207 | Log.MessageNetworked("Config File of DebugToolkit / Permission System successfully reloaded.", args, Log.LogLevel.Info); 208 | } 209 | 210 | [ConCommand(commandName = "perm_enable", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.PERM_ENABLE_HELP)] 211 | [AutoComplete(Lang.PERM_ENABLE_ARGS)] 212 | [RequiredLevel(Level.Admin)] 213 | private static void CCPermissionEnable(ConCommandArgs args) 214 | { 215 | if (args.Count == 0) 216 | { 217 | IsEnabled.Value = !IsEnabled.Value; 218 | } 219 | else 220 | { 221 | if (!Util.TryParseBool(args[0], out bool value)) 222 | { 223 | Log.MessageNetworked(String.Format(Lang.PARSE_ERROR, "value", "bool"), args, Log.LogLevel.ErrorClientOnly); 224 | return; 225 | } 226 | IsEnabled.Value = value; 227 | } 228 | 229 | var res = string.Format(IsEnabled.Value ? Lang.SETTING_ENABLED : Lang.SETTING_DISABLED, "Permission System"); 230 | if (IsEnabled.Value) 231 | { 232 | res += ". Saving and reloading the permission system."; 233 | } 234 | 235 | Log.MessageNetworked(res, args, Log.LogLevel.Info); 236 | 237 | DebugToolkit.Configuration.Save(); 238 | Init(); 239 | } 240 | 241 | [ConCommand(commandName = "perm_mod", flags = ConVarFlags.ExecuteOnServer, helpText = Lang.PERM_MOD_HELP)] 242 | [AutoComplete(Lang.PERM_MOD_ARGS)] 243 | [RequiredLevel(Level.Admin)] 244 | private static void CCPermissionAddUser(ConCommandArgs args) 245 | { 246 | if (!IsEnabled.Value) 247 | { 248 | Log.MessageNetworked("The permission system is currently disabled, enable it first with perm_enable", args); 249 | return; 250 | } 251 | if (args.Count < 2) 252 | { 253 | Log.MessageNetworked(Lang.INSUFFICIENT_ARGS + Lang.PERM_MOD_ARGS, args, Log.LogLevel.MessageClientOnly); 254 | return; 255 | } 256 | NetworkUser nu = Util.GetNetUserFromString(args.userArgs, 1); 257 | if (nu == null) 258 | { 259 | Log.MessageNetworked(Lang.PLAYER_NOTFOUND, args, Log.LogLevel.MessageClientOnly); 260 | return; 261 | } 262 | var userSteamId = (ulong)nu.GetNetworkPlayerName().playerId.value; 263 | if (!Enum.TryParse(args[0], out Level level)) 264 | { 265 | Log.MessageNetworked(string.Format(Lang.PARSE_ERROR, "permission_level", "Level"), args, Log.LogLevel.MessageClientOnly); 266 | return; 267 | } 268 | switch (level) 269 | { 270 | case Level.Admin: 271 | if (!AdminSteamIdList.Contains(userSteamId)) 272 | { 273 | AdminSteamIdList.Add(userSteamId); 274 | _adminList.Value = string.Join(", ", AdminSteamIdList); 275 | } 276 | if (SubAdminSteamIdList.Contains(userSteamId)) 277 | { 278 | SubAdminSteamIdList.Remove(userSteamId); 279 | _subAdminList.Value = string.Join(", ", SubAdminSteamIdList); 280 | } 281 | break; 282 | case Level.SubAdmin: 283 | if (AdminSteamIdList.Contains(userSteamId)) 284 | { 285 | AdminSteamIdList.Remove(userSteamId); 286 | _adminList.Value = string.Join(", ", AdminSteamIdList); 287 | } 288 | if (!SubAdminSteamIdList.Contains(userSteamId)) 289 | { 290 | SubAdminSteamIdList.Add(userSteamId); 291 | _subAdminList.Value = string.Join(", ", SubAdminSteamIdList); 292 | } 293 | break; 294 | case Level.None: 295 | if (AdminSteamIdList.Contains(userSteamId)) 296 | { 297 | AdminSteamIdList.Remove(userSteamId); 298 | _adminList.Value = string.Join(", ", AdminSteamIdList); 299 | } 300 | if (SubAdminSteamIdList.Contains(userSteamId)) 301 | { 302 | SubAdminSteamIdList.Remove(userSteamId); 303 | _subAdminList.Value = string.Join(", ", SubAdminSteamIdList); 304 | } 305 | break; 306 | default: 307 | Log.MessageNetworked("Inappropriate value for 'permission level', available levels : None (0), SubAdmin (1), Admin (2)", args, Log.LogLevel.MessageClientOnly); 308 | return; 309 | } 310 | Log.MessageNetworked($"{nu.userName}'s permission level has been updated to {level}", args); 311 | } 312 | 313 | public static bool CanUserExecute(NetworkUser networkUser, string conCommandName, List<string> userArgs) 314 | { 315 | if (AdminCommands.TryGetValue(conCommandName, out var requiredLevel)) 316 | { 317 | var userLevel = networkUser.GetPermissionLevel(); 318 | 319 | if (userLevel >= requiredLevel.Value) 320 | return true; 321 | 322 | var conCommandArgs = new ConCommandArgs 323 | { 324 | commandName = conCommandName, 325 | sender = networkUser, 326 | userArgs = userArgs 327 | }; 328 | 329 | Log.MessageNetworked(string.Format(Lang.PS_NO_REQUIRED_LEVEL, requiredLevel.Value.ToString()), conCommandArgs); 330 | return false; 331 | } 332 | 333 | return true; 334 | } 335 | 336 | public static bool HasMorePerm(NetworkUser sender, NetworkUser target, ConCommandArgs args) 337 | { 338 | var senderElevationLevel = sender ? sender.GetPermissionLevel() : Level.Admin + 1; // +1 for server console 339 | var targetElevationLevel = target.GetPermissionLevel(); 340 | 341 | if (senderElevationLevel < targetElevationLevel) 342 | { 343 | Log.MessageNetworked(string.Format(Lang.PS_ARGUSER_HAS_MORE_PERM, target.userName), args, Log.LogLevel.Error); 344 | return false; 345 | } 346 | 347 | if (senderElevationLevel == targetElevationLevel) 348 | { 349 | Log.MessageNetworked(string.Format(Lang.PS_ARGUSER_HAS_SAME_PERM, target.userName), args, Log.LogLevel.Error); 350 | return false; 351 | } 352 | 353 | return true; 354 | } 355 | 356 | private static Level GetPermissionLevel(this NetworkUser networkUser) 357 | { 358 | var userSteamId = (ulong)networkUser.GetNetworkPlayerName().playerId.value; 359 | 360 | if (AdminSteamIdList.Contains(userSteamId)) 361 | return Level.Admin; 362 | 363 | if (SubAdminSteamIdList.Contains(userSteamId)) 364 | return Level.SubAdmin; 365 | 366 | return Level.None; 367 | } 368 | } 369 | } -------------------------------------------------------------------------------- /Code/Util.cs: -------------------------------------------------------------------------------- 1 | using RoR2; 2 | using RoR2.ExpansionManagement; 3 | using System.Collections.Generic; 4 | 5 | namespace DebugToolkit 6 | { 7 | public static class Util 8 | { 9 | /// <summary> 10 | /// Returns a matched NetworkUser when provided to a player. 11 | /// </summary> 12 | /// <param name="args">(string[])args array</param> 13 | /// <param name="startLocation">(int)on the string array, at which index the player string is. Default value is 0</param> 14 | /// <returns>Returns a NetworkUser if a match is found, or null if not</returns> 15 | internal static NetworkUser GetNetUserFromString(List<string> args, int index = 0) 16 | { 17 | if (args.Count > 0 && index < args.Count) 18 | { 19 | return StringFinder.Instance.GetPlayerFromPartial(args[index]); 20 | } 21 | return null; 22 | } 23 | 24 | /// <summary> 25 | /// Find the target CharacterMaster for a matched player string or pinged entity. 26 | /// </summary> 27 | /// <param name="args">(ConCommandArgs)command arguments</param> 28 | /// <param name="index">(int)on the string array, at which index the target string is</param> 29 | /// <returns>Returns the found master. Null otherwise</returns> 30 | internal static CharacterMaster GetTargetFromArgs(ConCommandArgs args, int index) 31 | { 32 | if (args.Count > 0 && index < args.Count) 33 | { 34 | if (args.sender != null && args[index].ToUpperInvariant() == Lang.PINGED) 35 | { 36 | return Hooks.GetPingedTarget(args.senderMaster).master; 37 | } 38 | return GetNetUserFromString(args.userArgs, index)?.master; 39 | } 40 | return null; 41 | } 42 | 43 | /// <summary> 44 | /// Get the user-friendly expansion name. 45 | /// </summary> 46 | /// <param name="expansion">(ExpansionDef)expansion definition</param> 47 | /// <returns>Returns the expansion name or the empty string</returns> 48 | internal static string GetExpansion(ExpansionDef expansion) 49 | { 50 | return expansion && !string.IsNullOrEmpty(expansion.nameToken) ? Language.GetString(expansion.nameToken) : ""; 51 | } 52 | 53 | /// <summary> 54 | /// Try to parse a bool that's either formatted as "true"/"false" or a whole number "0","1". Values above 0 are considered "truthy" and values equal or lower than zero are considered "false". 55 | /// </summary> 56 | /// <param name="input">the string to parse</param> 57 | /// <param name="result">the result if parsing was correct.</param> 58 | /// <returns>True if the string was parsed correctly. False otherwise</returns> 59 | internal static bool TryParseBool(string input, out bool result) 60 | { 61 | if (bool.TryParse(input, out result)) 62 | { 63 | return true; 64 | } 65 | if (int.TryParse(input, out int val)) 66 | { 67 | result = val > 0; 68 | return true; 69 | } 70 | return false; 71 | } 72 | 73 | internal struct CommandTarget 74 | { 75 | internal string failMessage; 76 | internal string name; 77 | internal CharacterBody body; 78 | internal Inventory inventory; 79 | internal DevotionInventoryController devotionController; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /DebugToolkit.csproj: -------------------------------------------------------------------------------- 1 | <Project Sdk="Microsoft.NET.Sdk"> 2 | 3 | <PropertyGroup> 4 | <TargetFramework>netstandard2.1</TargetFramework> 5 | <Version>3.19.3</Version> 6 | <GeneratePackageOnBuild>false</GeneratePackageOnBuild> 7 | <Authors>DebugToolkit Contributors</Authors> 8 | <Description>DebugToolkit is an expansive list of console commands for Risk of Rain 2, intended to make forcing specific situation easier. This is supposed to help with testing of interactions, mods, and what else.</Description> 9 | <RepositoryUrl>https://github.com/harbingerofme/DebugToolkit</RepositoryUrl> 10 | <Configurations>Debug;Release;BLEEDING-EDGE;NO-UNET</Configurations> 11 | <PackageIconUrl></PackageIconUrl> 12 | <PackageLicenseFile>LICENSE</PackageLicenseFile> 13 | <ApplicationIcon>Resources\icon.ico</ApplicationIcon> 14 | <SignAssembly>false</SignAssembly> 15 | <PackageIcon>icon.png</PackageIcon> 16 | <Copyright>2025 DebugToolkit Contributors</Copyright> 17 | <AssemblyVersion>3.19.3.0</AssemblyVersion> 18 | <FileVersion>3.19.3.0</FileVersion> 19 | </PropertyGroup> 20 | 21 | <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> 22 | <LangVersion>latest</LangVersion> 23 | </PropertyGroup> 24 | 25 | <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='BLEEDING-EDGE|AnyCPU'"> 26 | <LangVersion>latest</LangVersion> 27 | <DefineConstants>BLEEDING;</DefineConstants> 28 | </PropertyGroup> 29 | 30 | <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> 31 | <LangVersion>latest</LangVersion> 32 | </PropertyGroup> 33 | 34 | <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='NO-UNET|AnyCPU'"> 35 | <LangVersion>latest</LangVersion> 36 | <DefineConstants>BLEEDING;NONETWORK;</DefineConstants> 37 | <Optimize>True</Optimize> 38 | </PropertyGroup> 39 | 40 | <ItemGroup> 41 | <None Include="icon.png"> 42 | <Pack>True</Pack> 43 | <PackagePath></PackagePath> 44 | </None> 45 | <None Include="LICENSE"> 46 | <Pack>True</Pack> 47 | <PackagePath></PackagePath> 48 | </None> 49 | <None Include="LICENSE"> 50 | <Pack>True</Pack> 51 | <PackagePath></PackagePath> 52 | </None> 53 | </ItemGroup> 54 | 55 | <ItemGroup> 56 | <None Remove=".editorconfig" /> 57 | <None Remove=".gitattributes" /> 58 | <None Remove=".gitignore" /> 59 | <None Remove=".gitmodules" /> 60 | <None Remove=".markdownlint.json" /> 61 | <None Remove="BUILDING.MD" /> 62 | <None Remove="CurrentCommit" /> 63 | <None Remove="DebugToolkit.dll" /> 64 | <None Remove="DebugToolkit.zip" /> 65 | <None Remove="FodyWeavers.xml" /> 66 | <None Remove="GetVer.ps1" /> 67 | <None Remove="icon.png" /> 68 | <None Remove="icon.xcf" /> 69 | <None Remove="LICENSE" /> 70 | <None Remove="manifest.json" /> 71 | <None Remove="manifest.template" /> 72 | <None Remove="README.md" /> 73 | <None Remove="readmetest.zip" /> 74 | </ItemGroup> 75 | 76 | <ItemGroup> 77 | <EmbeddedResource Include="Resources\CurrentCommit" Condition="'$(Configuration)|$(Platform)'!='Release|AnyCPU'"> 78 | <CopyToOutputDirectory>Always</CopyToOutputDirectory> 79 | </EmbeddedResource> 80 | </ItemGroup> 81 | 82 | 83 | <ItemGroup> 84 | <Folder Include="libs\" /> 85 | <Folder Include="Properties\" /> 86 | </ItemGroup> 87 | 88 | 89 | <ItemGroup> 90 | <PackageReference Include="BepInEx.Core" Version="5.4.21" GeneratePathProperty="true" /> 91 | <PackageReference Include="MMHOOK.RoR2" Version="2025.5.5" NoWarn="NU1701" /> 92 | <PackageReference Include="RiskOfRain2.GameLibs" Version="1.3.8-r.0" GeneratePathProperty="true" /> 93 | <PackageReference Include="UnityEngine.Modules" Version="2021.3.33" GeneratePathProperty="true" /> 94 | </ItemGroup> 95 | 96 | <ItemGroup> 97 | <PackageReference Include="R2API.Core" Version="5.1.5" /> 98 | <PackageReference Include="R2API.ContentManagement" Version="1.0.9" /> 99 | <PackageReference Include="R2API.Networking" Version="1.0.3" /> 100 | <PackageReference Include="R2API.Prefab" Version="1.0.4" /> 101 | 102 | <PackageDownload Include="NETStandard.Library.Ref" Version="[2.1.0]" /> 103 | </ItemGroup> 104 | 105 | <ItemGroup> 106 | <Reference Include="ShareSuite"> 107 | <HintPath>libs\ShareSuite.dll</HintPath> 108 | </Reference> 109 | </ItemGroup> 110 | 111 | <Target Name="CopyToOut" AfterTargets="PostBuildEvent"> 112 | <Exec Command="robocopy $(TargetDir) $(ProjectDir) $(TargetFileName) > nul" IgnoreExitCode="true" /> 113 | <Exec Command="del $(ProjectDir)$(TargetFileName).prepatch" IgnoreExitCode="true" /> 114 | </Target> 115 | 116 | <Target Name="Weave" AfterTargets="CopyToOut" Condition="'$(Configuration)|$(Platform)'!='NO-UNET|AnyCPU' and '$(Configuration)|$(Platform)'!='AnyCPU'"> 117 | <Exec Command="robocopy $(TargetDir) $(ProjectDir)NetworkWeaver\ $(TargetFileName) > nul" IgnoreExitCode="true" /> 118 | <Exec Command="echo moved to NetworkWeaver" /> 119 | <Exec Command="$(ProjectDir)NetworkWeaver\Unity.UNetWeaver.exe "$(PkgUnityEngine_Modules)\lib\netstandard2.0\UnityEngine.CoreModule.dll" "$(PkgRiskOfRain2_GameLibs)\lib\netstandard2.0\com.unity.multiplayer-hlapi.Runtime.dll" "$(ProjectDir)\NetworkWeaver\Patched/" "$(ProjectDir)NetworkWeaver\$(TargetFileName)" "$(ProjectDir)\libs" "$(PkgUnityEngine_Modules)\lib\netstandard2.0" "$(PkgRiskOfRain2_GameLibs)\lib\netstandard2.0" "$(NugetPackageRoot)\netstandard.library.ref\2.1.0\ref\netstandard2.1"" /> 120 | <Exec Command="echo weaved" /> 121 | <Exec Command="ren $(TargetFileName) $(TargetFileName).prepatch" IgnoreExitCode="true" /> 122 | <Exec Command="echo renamed prenetwork file to prepatch for backup" /> 123 | <Exec Command="robocopy $(ProjectDir)NetworkWeaver\Patched $(ProjectDir) $(TargetFileName) /mov > nul" IgnoreExitCode="true" /> 124 | <Exec Command="echo moved patched file to project folder" /> 125 | </Target> 126 | 127 | <Target Name="getGitCommit" BeforeTargets="PreBuildEvent" Condition="'$(Configuration)|$(Platform)'!='Release|AnyCPU'"> 128 | <Exec Command="git rev-parse --verify HEAD > Resources\CurrentCommit " /> 129 | </Target> 130 | 131 | <ProjectExtensions> 132 | <VisualStudio> 133 | <UserProperties thunderstore_4contents_4manifest_1json__JsonSchema="" /> 134 | </VisualStudio> 135 | </ProjectExtensions> 136 | 137 | </Project> 138 | -------------------------------------------------------------------------------- /DebugToolkit.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.0.31912.275 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DebugToolkit", "DebugToolkit.csproj", "{38FD0DD5-5D4F-424B-9590-2DB309099D17}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Items", "Items", "{841CDF50-F197-4B30-AE96-270AE80E6032}" 8 | ProjectSection(SolutionItems) = preProject 9 | .gitattributes = .gitattributes 10 | .gitignore = .gitignore 11 | .gitmodules = .gitmodules 12 | BUILDING.MD = BUILDING.MD 13 | CHANGELOG.md = CHANGELOG.md 14 | FodyWeavers.xml = FodyWeavers.xml 15 | LICENSE = LICENSE 16 | README.md = README.md 17 | readmetest.zip = readmetest.zip 18 | EndProjectSection 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | BLEEDING-EDGE|Any CPU = BLEEDING-EDGE|Any CPU 23 | Debug|Any CPU = Debug|Any CPU 24 | NO-UNET|Any CPU = NO-UNET|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {38FD0DD5-5D4F-424B-9590-2DB309099D17}.BLEEDING-EDGE|Any CPU.ActiveCfg = BLEEDING-EDGE|Any CPU 29 | {38FD0DD5-5D4F-424B-9590-2DB309099D17}.BLEEDING-EDGE|Any CPU.Build.0 = BLEEDING-EDGE|Any CPU 30 | {38FD0DD5-5D4F-424B-9590-2DB309099D17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {38FD0DD5-5D4F-424B-9590-2DB309099D17}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {38FD0DD5-5D4F-424B-9590-2DB309099D17}.NO-UNET|Any CPU.ActiveCfg = NO-UNET|Any CPU 33 | {38FD0DD5-5D4F-424B-9590-2DB309099D17}.NO-UNET|Any CPU.Build.0 = NO-UNET|Any CPU 34 | {38FD0DD5-5D4F-424B-9590-2DB309099D17}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {38FD0DD5-5D4F-424B-9590-2DB309099D17}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {4BF56163-B166-429B-9C03-BD82980395F6} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /FodyWeavers.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> 3 | <Costura> 4 | <IncludeAssemblies> 5 | </IncludeAssemblies> 6 | </Costura> 7 | </Weavers> -------------------------------------------------------------------------------- /FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> 3 | <!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> 4 | <xs:element name="Weavers"> 5 | <xs:complexType> 6 | <xs:all> 7 | <xs:element name="Costura" minOccurs="0" maxOccurs="1"> 8 | <xs:complexType> 9 | <xs:all> 10 | <xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string"> 11 | <xs:annotation> 12 | <xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation> 13 | </xs:annotation> 14 | </xs:element> 15 | <xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string"> 16 | <xs:annotation> 17 | <xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation> 18 | </xs:annotation> 19 | </xs:element> 20 | <xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string"> 21 | <xs:annotation> 22 | <xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with line breaks.</xs:documentation> 23 | </xs:annotation> 24 | </xs:element> 25 | <xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string"> 26 | <xs:annotation> 27 | <xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with line breaks.</xs:documentation> 28 | </xs:annotation> 29 | </xs:element> 30 | <xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string"> 31 | <xs:annotation> 32 | <xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation> 33 | </xs:annotation> 34 | </xs:element> 35 | </xs:all> 36 | <xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean"> 37 | <xs:annotation> 38 | <xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation> 39 | </xs:annotation> 40 | </xs:attribute> 41 | <xs:attribute name="IncludeDebugSymbols" type="xs:boolean"> 42 | <xs:annotation> 43 | <xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation> 44 | </xs:annotation> 45 | </xs:attribute> 46 | <xs:attribute name="DisableCompression" type="xs:boolean"> 47 | <xs:annotation> 48 | <xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation> 49 | </xs:annotation> 50 | </xs:attribute> 51 | <xs:attribute name="DisableCleanup" type="xs:boolean"> 52 | <xs:annotation> 53 | <xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation> 54 | </xs:annotation> 55 | </xs:attribute> 56 | <xs:attribute name="LoadAtModuleInit" type="xs:boolean"> 57 | <xs:annotation> 58 | <xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation> 59 | </xs:annotation> 60 | </xs:attribute> 61 | <xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean"> 62 | <xs:annotation> 63 | <xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation> 64 | </xs:annotation> 65 | </xs:attribute> 66 | <xs:attribute name="ExcludeAssemblies" type="xs:string"> 67 | <xs:annotation> 68 | <xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation> 69 | </xs:annotation> 70 | </xs:attribute> 71 | <xs:attribute name="IncludeAssemblies" type="xs:string"> 72 | <xs:annotation> 73 | <xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation> 74 | </xs:annotation> 75 | </xs:attribute> 76 | <xs:attribute name="Unmanaged32Assemblies" type="xs:string"> 77 | <xs:annotation> 78 | <xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with |.</xs:documentation> 79 | </xs:annotation> 80 | </xs:attribute> 81 | <xs:attribute name="Unmanaged64Assemblies" type="xs:string"> 82 | <xs:annotation> 83 | <xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with |.</xs:documentation> 84 | </xs:annotation> 85 | </xs:attribute> 86 | <xs:attribute name="PreloadOrder" type="xs:string"> 87 | <xs:annotation> 88 | <xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation> 89 | </xs:annotation> 90 | </xs:attribute> 91 | </xs:complexType> 92 | </xs:element> 93 | </xs:all> 94 | <xs:attribute name="VerifyAssembly" type="xs:boolean"> 95 | <xs:annotation> 96 | <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> 97 | </xs:annotation> 98 | </xs:attribute> 99 | <xs:attribute name="VerifyIgnoreCodes" type="xs:string"> 100 | <xs:annotation> 101 | <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> 102 | </xs:annotation> 103 | </xs:attribute> 104 | <xs:attribute name="GenerateXsd" type="xs:boolean"> 105 | <xs:annotation> 106 | <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> 107 | </xs:annotation> 108 | </xs:attribute> 109 | </xs:complexType> 110 | </xs:element> 111 | </xs:schema> -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019-2022, "harbingerofme, "Paddywaan", "xiaoxiao921" 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /NetworkWeaver/Unity.Cecil.Mdb.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/NetworkWeaver/Unity.Cecil.Mdb.dll -------------------------------------------------------------------------------- /NetworkWeaver/Unity.Cecil.Pdb.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/NetworkWeaver/Unity.Cecil.Pdb.dll -------------------------------------------------------------------------------- /NetworkWeaver/Unity.Cecil.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/NetworkWeaver/Unity.Cecil.dll -------------------------------------------------------------------------------- /NetworkWeaver/Unity.UNetWeaver.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/NetworkWeaver/Unity.UNetWeaver.exe -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DebugToolkit 2 | 3 | This mod adds various debugging commands to the console. See below for all commands plus explanation. 4 | 5 | [Harb](https://thunderstore.io/package/Harb/), [iDeathHD](https://thunderstore.io/package/xiaoxiao921/) and ['s](https://thunderstore.io/package/paddywan/) reimplementation of [Morris1927's](https://thunderstore.io/package/Morris1927/) [RoR2Cheats](https://thunderstore.io/package/Morris1927/RoR2Cheats/). Derived with permission. 6 | 7 | Also adds autocompletion for arguments and networked commands giving their information to the right people to the console. Read [here](https://github.com/harbingerofme/DebugToolkit/tree/master/Code/AutoCompletion) to see how you can use the autocompletion feature for your own mod. 8 | 9 | Track update progress, get support and suggest new features over at the [DebugToolkit discord](https://discord.gg/yTfsMWP). 10 | 11 | Some vanilla console functions you might not know: 12 | 13 | * The console can be opened with `ctrl+alt+~`. 14 | * `help {command}` may be used to get help for a specific command 15 | * `find {term}` can be used to find commands with that term. 16 | * `max_messages {nr}` changes how much scroll back the console has. We auto change this to 100 for you if it's on default. 17 | 18 | Mods recommended for combined use: 19 | 20 | * [KeyBindForConsole](https://thunderstore.io/package/kristiansja/KeyBindForConsole/) for easier enabling of the console. Especially useful for non-US keyboard layouts. 21 | * [R2DSE](https://thunderstore.io/package/Harb/R2DSEssentials/) for running DT on dedicated servers. 22 | * [MidRunArtifacts](https://thunderstore.io/package/KingEnderBrine/MidRunArtifacts/) for enabling and disabling artifacts during a run. 23 | 24 | You may contact us at any time through [issues on GitHub](https://github.com/harbingerofme/DebugToolkit/issues/new/choose), the [dedicated discord server](https://discord.gg/yTfsMWP) or through the [Risk of Rain 2 modding Discord](https://discord.gg/5MbXZvd) found at the top of the Thunderstore website. 25 | 26 | --- 27 | 28 | ## Additional Contributors ## 29 | 30 | * [DestroyedClone](https://thunderstore.io/package/DestroyedClone/) ([Github](https://github.com/DestroyedClone)) 31 | * [Rays](https://github.com/SuperRayss) 32 | * [HeyImNoop](https://thunderstore.io/package/Heyimnoob/) 33 | * [SChinchi](https://github.com/SChinchi) 34 | * [RandomlyAwesome](https://github.com/yekoc) 35 | 36 | --- 37 | 38 | ## COMMANDS ## 39 | 40 | Command Parsing: 41 | - Multiple commands can be combined with `;`. 42 | - Only alphanumeric characters and the symbols `._-:` are allowed. Any other character requires being surrounded by quotes in order to be parsed literally. Both `"` and `'` work, while `\"` and `\'` also work when nesting quotes. 43 | 44 | Verbiage: 45 | - The brackets encapsulating arguments mean `{needed}`, `(choose one)`, and `[optional]`. 46 | - If an optional argument has a default value, it will be indicated with `:`. Any preceeding optional arguments from the one entered become necessary, but empty double quotes (`""`) can be used as a placeholder for the default value. If an optional argument is required from a dedicated server, it will be preceeded by `*`. 47 | - Player, body, AI, item, equipment, team, elite, interactable, and director card values can be declared by either their ID, or their string name. The latter can be written in freeform and it will be matched to the first result that contains the string. See the related `list_` commands for options and which result would take precedence if there are multiple matches. 48 | 49 | Commands: 50 | 51 | * **next_stage** - Advance to the next stage. `next_stage [specific_stage]`. If no stage is entered, the next stage in progression is selected. 52 | * **force_family_event** - Forces a Family Event to happen in the next stage, takes no arguments. `force_family_event` 53 | * **next_boss** - Sets the next teleporter/simulacrum boss to the specified boss. Get a list of potential boss with `list_directorcards`. `next_boss {director_card} [count:1] [elite:None]` 54 | * **fixed_time** - Sets the time that has progressed in the run. Affects difficulty. `fixed_time [time]`. If no time is supplied, prints the current time to console. 55 | * **next_wave** - Advance to the next Simulacrum wave. `next_wave` 56 | * **force_wave** - Set the next wave prefab. `force_wave [wave_prefab]`. If no input is supplied, prints all available options and clears any previous selection. 57 | * **run_set_waves_cleared** - Set the Simulacrum waves cleared. Must be positive. `set_run_waves_cleared {wave}` 58 | * **add_portal** - Add a portal to the current Teleporter on completion. `add_portal {portal ('blue'|'celestial'|'gold'|'green'|'void'|'all')}`. 59 | * **charge_zone** - Set the charge of all active holdout zones. `charge_zone {charge}`. The value is a float between 0 and 100. 60 | * **seed** - Set the seed for all next runs this session. `seed [new_seed]`. Use `0` to specify the game should generate its own seed. If used without argument, it's equivalent to the vanilla `run_get_seed`. 61 | * **set_artifact** - Enable/disable an Artifact. `set_artifact {artifact (artifact|'all')} [enable (0|1)]`. If enable isn't supplied, it will toggle the artifact's current state. However, it is required when using "all". 62 | * **kill_all** - Kills all members of a specified team. `kill_all [team:Monster]`. 63 | * **true_kill** - Truly kill a player, ignoring revival effects. `true_kill *[player:<self>]` 64 | * **respawn** - Respawn a player at the map spawnpoint. `respawn *[player:<self>]` 65 | * **hurt** - Deal generic damage to a target. `hurt {amount} *[target (player|'pinged'):<self>]*` 66 | * **heal** - Heal a target. `heal {amount} *[target (player|'pinged'):<self>]*` 67 | * **teleport_on_cursor** - Teleport you to where your cursor is currently aiming at. `teleport_on_cursor` 68 | * **time_scale** - Sets the timescale of the game. 0.5 would mean everything happens at half speed. `time_scale [time_scale]`. If no argument is supplied, gives the current timescale. 69 | * **post_sound_event** - Post a sound event to the AkSoundEngine (WWise) either by its event name or event ID. `post_sound_event {sound_event (event_name|event_id)}` 70 | 71 | List Commands: 72 | 73 | * [All the `list_` commands support filtering](https://user-images.githubusercontent.com/57867641/295963274-169b2fd9-a5ea-41df-8dba-2632f75ddbd4.png). A number for the unique index or any string for partial matching. 74 | * **list_player** - List all Players and their ID. 75 | * **list_body** - List all Bodies and their language invariants. 76 | * **list_ai** - List all Masters and their language invariants. 77 | * **list_elite** - List all Elites and their language invariants. 78 | * **list_team** - List all Teams and their language invariants. 79 | * **list_artifact** - List all Artifacts and their language invariants. 80 | * **list_buff** - List all Buffs and if they are stackable. 81 | * **list_dot** - List all DoT effects. 82 | * **list_itemtier** - List all Item Tiers. 83 | * **list_item** - List all Items, their language invariants, and if they are in the current drop pool. 84 | * **list_equip** - List all Equipment, their language invariants, and if they are in the current drop pool. 85 | * **list_interactables/list_interactibles** List all Interactables. 86 | * **list_directorcards** List all Director Cards. Mainly used for the `next_boss` command. 87 | * **list_scene** List all Scenes, their language invariants, and if are they an offline scene. 88 | * **list_skins** List all Body Skins and the language invariant of the current one in use. 89 | * **list_survivor** List all Survivors and their body/ai names. 90 | 91 | Dump Commands: 92 | 93 | * **dump_buffs** - List the buffs/debuffs of all spawned bodies. 94 | * **dump_inventories** - List the inventory items and equipment of all spawned bodies. 95 | * **dump_state** - List the current stats, entity state, and skill cooldown of a specified body. `dump_state *[target (player|'pinged'):<self>]` 96 | * **dump_stats** - List the base stats of a specific body. 97 | 98 | Buff Commands: 99 | 100 | * **give_buff** - Gives a buff to a character. Duration of 0 means permanent: `give_buff {buff} [count:1] [duration:0] *[target (player|'pinged'):<self>]` 101 | * **give_dot** - Gives a DoT stack to a character: `give_dot {dot} [count:1] *[target (player|'pinged'):<self>] *[attacker (player|'pinged'):<self>]` 102 | * **remove_buff** - Removes a buff from a character. Timed buffs prioritise the longest expiration stack: `remove_buff {buff} [count:1] [timed (0|1):0/false] *[target (player|'pinged'):<self>]` 103 | * **remove_buff_stacks** - Resets a buff for a character: `remove_buff_stacks {buff} [timed (0|1):0/false] *[target (player|'pinged'):<self>]` 104 | * **remove_all_buffs** - Resets all buffs for a character: `remove_all_buffs [timed (0|1):0/false] *[target (player|'pinged'):<self>]` 105 | * **remove_dot** - Removes a DoT stack with the longest expiration from a character: `remove_dot {dot} [count:1] *[target (player|'pinged'):<self>]` 106 | * **remove_dot_stacks** - Removes all stacks of a DoT effect from a character: `remove_dot_stacks {dot} *[target (player|'pinged'):<self>]` 107 | * **remove_all_dots** - Removes all DoT effects from a character: `remove_all_dot *[target (player|'pinged'):<self>]` 108 | 109 | Item Commands: 110 | 111 | * **give_item** - Give an item directly to a target's inventory. A negative amount is an alias for `remove_item`: `give_item {item} [count:1] *[target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]` 112 | * **random_items** - Generate random items from the available item tiers. `random_items {count} [droptable (droptable|'all'):'all'] *[target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]` 113 | * **give_equip** - Give an equipment directly to a target's inventory: `give_equip {(equip|'random')} *[target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]` 114 | * **give_money** - Gives the desired player/team money. A negative amount can remove that many without underflowing. `give_money {amount} [target ('all'|player):'all']` 115 | * **give_lunar** - Gives the specified amount of lunar coins to the issuing player. A negative count may be specified to remove that many. `give_lunar [amount:1]` 116 | * **remove_item** - Removes an item from a target's inventory. A negative amount is an alias for `give_item`: `remove_item {item} [count:1] *[target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]` 117 | * **remove_item_stacks** - Removes all item stacks from a target's inventory. `remove_item_stacks {item} *[target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]` 118 | * **remove_all_items** - Removes all items from a target's inventory. `remove_all_items *[target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]` 119 | * **remove_equip** - Sets the equipment of a target to 'None'. `remove_equip *[target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]` 120 | * **restock_equip** - Restock charges for the current equipment. `restock_equip [count:1] *[target (player|'pinged'|'evolution'|'simulacrum'|'voidfields'|'devotion'):<self>]` 121 | * **create_pickup** - Creates a pickup in front of a player. Pickups are items, equipment, or coins. When the pickup is an item or equipment, the search argument 'item' or 'equip' may be specified to only search that list. `create_pickup {object (item|equip|'lunarcoin'|'voidcoin')} [search ('item'|'equip'|'both'):'both'] *[player:<self>]` 122 | * **create_potential** - Creates a potential in front of a player. The first item tier defined in the droptable decides the color of the droplet and what items will be available with the Artifact of Command. `create_potential [droptable (droptable|'all'):'all'] [count:3] *[player:<self>]` 123 | 124 | ***Note:*** Some commands support a weighted item selection, referred to as _droptable_. The syntax for it is `<itemtier:weight tokens separated by comma>`. The weight should be a positive float and is an optional argument with a default value of 1.0. If a comma or decimal point is used, the whole argument must be surrounded in double quotes. The keyword `all` uses all available item tiers with a default weight. For example, any of the following are valid inputs: `tier1`, `"tier1:5,tier2,tier3:0.4"`, `all`. 125 | 126 | Spawn Commands: 127 | 128 | * **spawn_interactable/spawn_interactible** - Spawns an interactible in front of the player. `(spawn_interactable|spawn_interactible) {interactable}` 129 | * **spawn_portal** - Spawns a portal in front of the player. `spawn_portal {portal ('artifact'|'blue'|'celestial'|'deepvoid'|'gold'|'green'|'null'|'void')}`. 130 | * **spawn_ai** - Spawn an AI. `spawn_ai {ai} [count:1] [elite:None] [braindead (0|1):0/false] [team:Monster]`. 131 | * **spawn_as** - Spawn as a new character. `spawn_as {body} *[player:<self>]` 132 | * **spawn_body** - Spawns a CharacterBody with no AI, inventory, or team alliance: `spawn_body {body}` 133 | * **change_team** - Change a player's team. `change_team {team} *[player:<self>]`. 134 | 135 | Profile Commands: 136 | 137 | * **prevent_profile_writing** - Prevent saving the user profile to avoid bogus data. Enable before doing something and keep it until the end of the session. `prevent_profile_writing [flag (0|1)]`. If no argument is supplied, prints the current state. Disabled by default. 138 | 139 | Cheat Commands: 140 | 141 | * **no_enemies** - Toggles enemy spawns. 142 | * **god** - Toggles HealthComponent.TakeDamage for all players. AKA: you can't take damage. 143 | * **buddha** / **budha** / **buda** / **budda** - Turns damage taken `NonLethal` for all players. AKA: you can't die. 144 | * **lock_exp** - Prevents EXP gain for the player team. 145 | * **noclip** - Toggles noclip. Allow you to fly and going through objects. Sprinting will double the speed. 146 | 147 | Bind Commands: 148 | 149 | * **dt_bind** - Bind a key to execute specific commands. `dt_bind {key} [<consolecommands seperated by ;>]`. See [here](https://docs.unity3d.com/Manual/class-InputManager.html) for a list of possible key names. Alt, Ctrl, and Shift can also be used for key combinations, e.g. `"left shift+left ctrl+x"`. If no commands are provided, it prints information about the key. 150 | * **dt_bind_delete** Remove a custom bind. `dt_bind_delete {key}` 151 | * **dt_bind_reload** Reload the macro system from file. `dt_bind_reload` 152 | 153 | Server Related Commands: 154 | 155 | * **kick** - Kicks the specified Player Name/ID from the game. 156 | * **ban** - Session bans the specified Player Name/ID from the game. 157 | 158 | * **perm_enable** - Enable or disable the permission system. 159 | * **perm_mod** - Change the permission level of the specified PlayerID/Username with the specified Permission Level. 160 | 161 | Reload Commands: 162 | 163 | * **perm_reload** - Reload the permission system, updates user and commands permissions. 164 | * **reload_all_config** - Reload all default config files from all loaded BepinEx plugins. 165 | 166 | ### Unlocked Vanilla Commands ### 167 | 168 | * **sv_time_transmit_interval** - How long it takes for the server to issue a time update to clients. `sv_time_transmit_interval [time]` 169 | * **run_scene_override** - Overrides the first scene to enter in a run. `run_scene_override [scene]` 170 | * **stage1_pod** - Whether or not to use the pod when spawning on the first stage. `stage1_pod [(0|1)]` 171 | * **run_set_stages_cleared** - Sets the amount of stages cleared. This does not change the current stage. `run_set_stages_cleared {stage_count}`. This obsoletes `stage_clear_count` from previous RoR2Cheats versions. 172 | * **team_set_level** - Sets the specified team to the specified level: `team_set_level {team} {level}`. This obsoletes `give_exp` from previous RoR2Cheats versions. 173 | * **loadout_set_skill_variant** - Sets the skill variant for the sender's user profile: `loadout_set_skill_variant {body_name} {skill_slot_index} {skill_variant_index}`. Note that this does not use the loose bodymatching from custom commands. 174 | * **set_scene** - Removed the cheat check on this. Functions similar but not really to our `next_stage`, doesn't have our cool autocomplete features, and doesn't advance the stage count, but can advance menus. `set_scene {scene}` 175 | 176 | ### Additional Macros ### 177 | 178 | * **midgame** - This is the preset HopooGames uses for midgame testing. Gives all users random items, and drops you off in the bazaar. `midgame` 179 | * **lategame** - This is the preset HopooGames uses for endgame testing. Gives all users random items, and drops you off in the bazaar. `lategame` 180 | * **dtzoom** - Gives you 20 hooves and 200 feathers to get around the map quickly. Based on a command in the initial release of Risk of Rain 2. 181 | -------------------------------------------------------------------------------- /Resources/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 |  2 | // This file is used by Code Analysis to maintain SuppressMessage 3 | // attributes that are applied to this project. 4 | // Project-level suppressions either have no target or are given 5 | // a specific target and scoped to a namespace, type, member, etc. 6 | 7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Code Quality", "IDE0051:Remove unused private members", Justification = "Console and Network commands will always be unused.")] 8 | 9 | -------------------------------------------------------------------------------- /Resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/Resources/icon.ico -------------------------------------------------------------------------------- /Thunderstore/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/Thunderstore/icon.png -------------------------------------------------------------------------------- /Thunderstore/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/Thunderstore/icon.xcf -------------------------------------------------------------------------------- /Thunderstore/thunderstore.toml: -------------------------------------------------------------------------------- 1 | [config] 2 | schemaVersion = "0.0.1" 3 | 4 | [package] 5 | namespace = "IHarbHD" 6 | name = "DebugToolkit" 7 | versionNumber = "3.19.3" 8 | description = "Adds console command for debugging mods." 9 | websiteUrl = "https://github.com/harbingerofme/DebugToolkit" 10 | containsNsfwContent = false 11 | 12 | [package.dependencies] 13 | bbepis-BepInExPack = "5.4.2117" 14 | RiskofThunder-R2API_Core = "5.1.5" 15 | RiskofThunder-R2API_ContentManagement = "1.0.9" 16 | RiskofThunder-R2API_Networking = "1.0.3" 17 | RiskofThunder-R2API_Prefab = "1.0.4" 18 | 19 | [build] 20 | icon = "./icon.png" 21 | readme = "../README.md" 22 | outdir = "./build" 23 | 24 | [[build.copy]] 25 | source = "../CHANGELOG.md" 26 | target = "./" 27 | 28 | [[build.copy]] 29 | source = "../LICENSE" 30 | target = "./" 31 | 32 | [[build.copy]] 33 | source = "../DebugToolkit.dll" 34 | target = "./" 35 | 36 | [publish] 37 | repository = "https://thunderstore.io" 38 | communities = ["riskofrain2"] 39 | categories = ["mods", "tools"] -------------------------------------------------------------------------------- /libs/MMHOOK_RoR2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/libs/MMHOOK_RoR2.dll -------------------------------------------------------------------------------- /libs/R2API-LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Risk of Thunder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/R2API.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/libs/R2API.dll -------------------------------------------------------------------------------- /libs/ShareSuite.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harbingerofme/DebugToolkit/6f6be72b831fa426ebdfea8a5b308b18ca1689a0/libs/ShareSuite.dll -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <configuration> 3 | <packageSources> 4 | <add key="BepInEx" value="https://nuget.bepinex.dev/v3/index.json" /> 5 | <add key="Thunderstore" value="https://nuget.windows10ce.com/nuget/v3/index.json" /> 6 | </packageSources> 7 | </configuration> 8 | --------------------------------------------------------------------------------