├── .editorconfig ├── .git-blame-ignore-revs ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.txt ├── NeosModLoader.sln ├── NeosModLoader ├── AssemblyFile.cs ├── AssemblyHider.cs ├── AssemblyLoader.cs ├── AutoRegisterConfigKeyAttribute.cs ├── ConfigurationChangedEvent.cs ├── DebugInfo.cs ├── DelegateExtensions.cs ├── ExecutionHook.cs ├── HarmonyWorker.cs ├── IgnoresAccessChecksToAttribute.cs ├── JsonConverters │ ├── EnumConverter.cs │ └── NeosPrimitiveConverter.cs ├── LoadedNeosMod.cs ├── Logger.cs ├── ModConfiguration.cs ├── ModConfigurationDefinitionBuilder.cs ├── ModConfigurationKey.cs ├── ModLoader.cs ├── ModLoaderConfiguration.cs ├── NeosMod.cs ├── NeosModBase.cs ├── NeosModLoader.csproj ├── NeosVersionReset.cs ├── Properties │ └── AssemblyInfo.cs ├── SplashChanger.cs ├── Util.cs └── Utility │ ├── EnumerableInjector.cs │ └── PlatformHelper.cs ├── README.md └── doc ├── config.md ├── directories.md ├── example_log.log ├── faq.md ├── how_nml_works.md ├── img ├── NeosProLauncher.png ├── NeosPublicSetup.png ├── add_non_steam_game.png ├── non_steam_game_properties_1.png ├── non_steam_game_properties_2.png ├── steam_game_properties.png └── windows_unblock.png ├── linux.md ├── making_mods.md ├── modloader_config.md ├── neos_guidelines.md ├── neos_standalone_setup.md ├── problem_solving_techniques.md ├── start_neos.bat └── troubleshooting.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | 12 | [*.yml] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | ## tabs standardization 2 | 321ac86f17d62425ebaaf909b8b493c04ea23afb 3 | 4 | ## prior standardization 5 | 7555f8b407aec05d9b69b754f373fe9e2345dcd1 6 | 7 | ## dotnet format 8 | ce83bfe4a26f3662e08b5a3f3089518ad54a51a0 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.cs text eol=lf 3 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # NeosModLoader Contributing Guidelines 2 | 3 | If you are interested in contributing to this project via issues or PRs, please follow these guidelines. 4 | 5 | ## Issue Guidelines 6 | 7 | If you are reporting a bug, please include a log, or at the very least the relevant stack trace. NeosModLoader logs to the Neos log by default (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Logs`). 8 | 9 | ## PR Guidelines 10 | 11 | If you want your PR to be approved and merged quickly, please read the following: 12 | 13 | ### Code Style 14 | 15 | If your PR does not follow the project's code style it **will not be approved**. Code style should match our [editor config](../.editorconfig). If you aren't sure, use your IDE's formatter (Analyze > Code Cleanup in Visual Studio). 16 | 17 | ### New Features 18 | 19 | Please consider the NML [design goals](#design-goals) before adding a new feature. If you aren't sure if your new feature makes sense for NML, I'm happy to talk with you about a potential new feature in our [discussions area](https://github.com/zkxs/NeosModLoader/discussions). 20 | 21 | ## Design Goals 22 | 23 | - NML should be kept as simple as possible. Its purpose is to load mods, not mod Neos itself. Additionally, NML is not intended to fix Neos bugs. Please do not attempt to add Neos bugfixes directly to NML. Instead, ensure there's an issue open on [the Neos issue tracker](https://github.com/Neos-Metaverse/NeosPublic/issues), and only consider making a mod if the Neos team is unable to provide a fix in a reasonably timeframe. 24 | - NML should only create APIs where the added API complexity is paid for by added ease of development for mod creators. If a proposed API is only useful to a very small percentage of mods, NeosModLoader probably isn't the place for it. 25 | - NML should try to prevent mod developers from shooting themselves in the foot. For example, NML only supports mods with a single NeosMod implementation. Instead of silently ignoring extra implementations in a mod, NML will instead throw an error message and abort loading the mod entirely. 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | ignore: 13 | - dependency-name: "Newtonsoft.Json" 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow handles all the CI checks, builds, and artifact publishing for NeosModLoader. 2 | 3 | name: Test, Build, and Release 4 | on: [push, pull_request] 5 | 6 | env: 7 | NeosPath: "${{ github.workspace }}/neos_install/" # set up in neos-modding-group/neos-plugin-setup-action 8 | 9 | jobs: 10 | build: 11 | if: github.repository == 'neos-modding-group/NeosModLoader' # prevent forks from unintentionally running this workflow 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout NML 15 | uses: actions/checkout@v4 16 | - name: setup build environment 17 | uses: neos-modding-group/neos-plugin-setup-action@master # This is where the Neos dependencies come from. Learn more at https://github.com/neos-modding-group/neos-plugin-setup-action 18 | - name: lint 19 | run: dotnet format --verbosity detailed --verify-no-changes ./NeosModLoader.sln 20 | - name: build headed 21 | run: | 22 | dotnet build ./NeosModLoader.sln --configuration Release "-property:Headless=false;CopyToLibraries=false" 23 | mv ./NeosModLoader/bin/Release/net462/NeosModLoader.dll ./NeosModLoader.dll 24 | - name: build headless 25 | if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master' # only build headless run for pushes to master or tags 26 | run: | 27 | dotnet build ./NeosModLoader.sln --configuration Release "-property:Headless=true;CopyToLibraries=false" 28 | mv ./NeosModLoader/bin/Release/net462/NeosModLoader.dll ./NeosModLoaderHeadless.dll 29 | - name: upload workflow artifacts 30 | if: startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master' # only publish workflow artifacts for pushes to master or tags 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: NeosModLoader 34 | path: | 35 | ./NeosModLoader.dll 36 | ./NeosModLoaderHeadless.dll 37 | if-no-files-found: error 38 | - name: upload release artifacts 39 | if: startsWith(github.ref, 'refs/tags/') # only publish release artifacts for pushes to tags 40 | uses: softprops/action-gh-release@v1 41 | with: 42 | draft: true # if creating a new release, make it a draft 43 | files: | 44 | ./NeosModLoader/bin/Release/net462/0Harmony.dll 45 | ./NeosModLoader.dll 46 | ./NeosModLoaderHeadless.dll 47 | fail_on_unmatched_files: true 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | ## 6 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Ww][Ii][Nn]32/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # ASP.NET Scaffolding 68 | ScaffoldingReadMe.txt 69 | 70 | # StyleCop 71 | StyleCopReport.xml 72 | 73 | # Files built by Visual Studio 74 | *_i.c 75 | *_p.c 76 | *_h.h 77 | *.ilk 78 | *.meta 79 | *.obj 80 | *.iobj 81 | *.pch 82 | *.pdb 83 | *.ipdb 84 | *.pgc 85 | *.pgd 86 | *.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.vspscc 96 | *.vssscc 97 | .builds 98 | *.pidb 99 | *.svclog 100 | *.scc 101 | 102 | # Chutzpah Test files 103 | _Chutzpah* 104 | 105 | # Visual C++ cache files 106 | ipch/ 107 | *.aps 108 | *.ncb 109 | *.opendb 110 | *.opensdf 111 | *.sdf 112 | *.cachefile 113 | *.VC.db 114 | *.VC.VC.opendb 115 | 116 | # Visual Studio profiler 117 | *.psess 118 | *.vsp 119 | *.vspx 120 | *.sap 121 | 122 | # Visual Studio Trace Files 123 | *.e2e 124 | 125 | # TFS 2012 Local Workspace 126 | $tf/ 127 | 128 | # Guidance Automation Toolkit 129 | *.gpState 130 | 131 | # ReSharper is a .NET coding add-in 132 | _ReSharper*/ 133 | *.[Rr]e[Ss]harper 134 | *.DotSettings.user 135 | 136 | # TeamCity is a build add-in 137 | _TeamCity* 138 | 139 | # DotCover is a Code Coverage Tool 140 | *.dotCover 141 | 142 | # AxoCover is a Code Coverage Tool 143 | .axoCover/* 144 | !.axoCover/settings.json 145 | 146 | # Coverlet is a free, cross platform Code Coverage Tool 147 | coverage*.json 148 | coverage*.xml 149 | coverage*.info 150 | 151 | # Visual Studio code coverage results 152 | *.coverage 153 | *.coveragexml 154 | 155 | # NCrunch 156 | _NCrunch_* 157 | .*crunch*.local.xml 158 | nCrunchTemp_* 159 | 160 | # MightyMoose 161 | *.mm.* 162 | AutoTest.Net/ 163 | 164 | # Web workbench (sass) 165 | .sass-cache/ 166 | 167 | # Installshield output folder 168 | [Ee]xpress/ 169 | 170 | # DocProject is a documentation generator add-in 171 | DocProject/buildhelp/ 172 | DocProject/Help/*.HxT 173 | DocProject/Help/*.HxC 174 | DocProject/Help/*.hhc 175 | DocProject/Help/*.hhk 176 | DocProject/Help/*.hhp 177 | DocProject/Help/Html2 178 | DocProject/Help/html 179 | 180 | # Click-Once directory 181 | publish/ 182 | 183 | # Publish Web Output 184 | *.[Pp]ublish.xml 185 | *.azurePubxml 186 | # Note: Comment the next line if you want to checkin your web deploy settings, 187 | # but database connection strings (with potential passwords) will be unencrypted 188 | *.pubxml 189 | *.publishproj 190 | 191 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 192 | # checkin your Azure Web App publish settings, but sensitive information contained 193 | # in these scripts will be unencrypted 194 | PublishScripts/ 195 | 196 | # NuGet Packages 197 | *.nupkg 198 | # NuGet Symbol Packages 199 | *.snupkg 200 | # The packages folder can be ignored because of Package Restore 201 | **/[Pp]ackages/* 202 | # except build/, which is used as an MSBuild target. 203 | !**/[Pp]ackages/build/ 204 | # Uncomment if necessary however generally it will be regenerated when needed 205 | #!**/[Pp]ackages/repositories.config 206 | # NuGet v3's project.json files produces more ignorable files 207 | *.nuget.props 208 | *.nuget.targets 209 | 210 | # Microsoft Azure Build Output 211 | csx/ 212 | *.build.csdef 213 | 214 | # Microsoft Azure Emulator 215 | ecf/ 216 | rcf/ 217 | 218 | # Windows Store app package directories and files 219 | AppPackages/ 220 | BundleArtifacts/ 221 | Package.StoreAssociation.xml 222 | _pkginfo.txt 223 | *.appx 224 | *.appxbundle 225 | *.appxupload 226 | 227 | # Visual Studio cache files 228 | # files ending in .cache can be ignored 229 | *.[Cc]ache 230 | # but keep track of directories ending in .cache 231 | !?*.[Cc]ache/ 232 | 233 | # Others 234 | ClientBin/ 235 | ~$* 236 | *~ 237 | *.dbmdl 238 | *.dbproj.schemaview 239 | *.jfm 240 | *.pfx 241 | *.publishsettings 242 | orleans.codegen.cs 243 | 244 | # Including strong name files can present a security risk 245 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 246 | #*.snk 247 | 248 | # Since there are multiple workflows, uncomment next line to ignore bower_components 249 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 250 | #bower_components/ 251 | 252 | # RIA/Silverlight projects 253 | Generated_Code/ 254 | 255 | # Backup & report files from converting an old project file 256 | # to a newer Visual Studio version. Backup files are not needed, 257 | # because we have git ;-) 258 | _UpgradeReport_Files/ 259 | Backup*/ 260 | UpgradeLog*.XML 261 | UpgradeLog*.htm 262 | ServiceFabricBackup/ 263 | *.rptproj.bak 264 | 265 | # SQL Server files 266 | *.mdf 267 | *.ldf 268 | *.ndf 269 | 270 | # Business Intelligence projects 271 | *.rdl.data 272 | *.bim.layout 273 | *.bim_*.settings 274 | *.rptproj.rsuser 275 | *- [Bb]ackup.rdl 276 | *- [Bb]ackup ([0-9]).rdl 277 | *- [Bb]ackup ([0-9][0-9]).rdl 278 | 279 | # Microsoft Fakes 280 | FakesAssemblies/ 281 | 282 | # GhostDoc plugin setting file 283 | *.GhostDoc.xml 284 | 285 | # Node.js Tools for Visual Studio 286 | .ntvs_analysis.dat 287 | node_modules/ 288 | 289 | # Visual Studio 6 build log 290 | *.plg 291 | 292 | # Visual Studio 6 workspace options file 293 | *.opt 294 | 295 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 296 | *.vbw 297 | 298 | # Visual Studio LightSwitch build output 299 | **/*.HTMLClient/GeneratedArtifacts 300 | **/*.DesktopClient/GeneratedArtifacts 301 | **/*.DesktopClient/ModelManifest.xml 302 | **/*.Server/GeneratedArtifacts 303 | **/*.Server/ModelManifest.xml 304 | _Pvt_Extensions 305 | 306 | # Paket dependency manager 307 | .paket/paket.exe 308 | paket-files/ 309 | 310 | # FAKE - F# Make 311 | .fake/ 312 | 313 | # CodeRush personal settings 314 | .cr/personal 315 | 316 | # Python Tools for Visual Studio (PTVS) 317 | __pycache__/ 318 | *.pyc 319 | 320 | NeosHeadless/* 321 | 322 | # Cake - Uncomment if you are using it 323 | # tools/** 324 | # !tools/packages.config 325 | 326 | # Tabs Studio 327 | *.tss 328 | 329 | # Telerik's JustMock configuration file 330 | *.jmconfig 331 | 332 | # BizTalk build output 333 | *.btp.cs 334 | *.btm.cs 335 | *.odx.cs 336 | *.xsd.cs 337 | 338 | # OpenCover UI analysis results 339 | OpenCover/ 340 | 341 | # Azure Stream Analytics local run output 342 | ASALocalRun/ 343 | 344 | # MSBuild Binary and Structured Log 345 | *.binlog 346 | 347 | # NVidia Nsight GPU debugger configuration file 348 | *.nvuser 349 | 350 | # MFractors (Xamarin productivity tool) working folder 351 | .mfractor/ 352 | 353 | # Local History for Visual Studio 354 | .localhistory/ 355 | 356 | # BeatPulse healthcheck temp database 357 | healthchecksdb 358 | 359 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 360 | MigrationBackup/ 361 | 362 | # Ionide (cross platform F# VS Code tools) working folder 363 | .ionide/ 364 | 365 | # Fody - auto-generated XML schema 366 | FodyWeavers.xsd 367 | 368 | # Specifically allow the example log 369 | !/doc/example_log.log 370 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /NeosModLoader.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31410.357 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NeosModLoader", "NeosModLoader\NeosModLoader.csproj", "{D4627C7F-8091-477A-ABDC-F1465D94D8D9}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {D4627C7F-8091-477A-ABDC-F1465D94D8D9}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {757072E6-E985-4EC2-AB38-C4D1588F6A15} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /NeosModLoader/AssemblyFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace NeosModLoader 5 | { 6 | internal class AssemblyFile 7 | { 8 | internal string File { get; } 9 | internal Assembly Assembly { get; set; } 10 | internal AssemblyFile(string file, Assembly assembly) 11 | { 12 | File = file; 13 | Assembly = assembly; 14 | } 15 | private string? sha256; 16 | internal string Sha256 17 | { 18 | get 19 | { 20 | if (sha256 == null) 21 | { 22 | try 23 | { 24 | sha256 = Util.GenerateSHA256(File); 25 | } 26 | catch (Exception e) 27 | { 28 | Logger.ErrorInternal($"Exception calculating sha256 hash for {File}:\n{e}"); 29 | sha256 = "failed to generate hash"; 30 | } 31 | } 32 | return sha256; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /NeosModLoader/AssemblyHider.cs: -------------------------------------------------------------------------------- 1 | using BaseX; 2 | using FrooxEngine; 3 | using HarmonyLib; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using System.Reflection; 9 | 10 | namespace NeosModLoader 11 | { 12 | internal static class AssemblyHider 13 | { 14 | /// 15 | /// Companies that indicate an assembly is part of .NET. 16 | /// This list was found by debug logging the AssemblyCompanyAttribute for all loaded assemblies. 17 | /// 18 | private static HashSet knownDotNetCompanies = new List() 19 | { 20 | "Mono development team", // used by .NET stuff and Mono.Security 21 | }.Select(company => company.ToLower()).ToHashSet(); 22 | 23 | /// 24 | /// Products that indicate an assembly is part of .NET. 25 | /// This list was found by debug logging the AssemblyProductAttribute for all loaded assemblies. 26 | /// 27 | private static HashSet knownDotNetProducts = new List() 28 | { 29 | "Microsoft® .NET", // used by a few System.* assemblies 30 | "Microsoft® .NET Framework", // used by most of the System.* assemblies 31 | "Mono Common Language Infrastructure", // used by mscorlib stuff 32 | }.Select(product => product.ToLower()).ToHashSet(); 33 | 34 | /// 35 | /// Assemblies that were already loaded when NML started up, minus a couple known non-Neos assemblies. 36 | /// 37 | private static HashSet? neosAssemblies; 38 | 39 | /// 40 | /// Assemblies that 100% exist due to a mod 41 | /// 42 | private static HashSet? modAssemblies; 43 | 44 | /// 45 | /// .NET assembiles we want to ignore in some cases, like the callee check for the AppDomain.GetAssemblies() patch 46 | /// 47 | private static HashSet? dotNetAssemblies; 48 | 49 | /// 50 | /// Patch Neos's type lookup code to not see mod-related types. This is needed, because users can pass 51 | /// arbitrary strings to TypeHelper.FindType(), which can be used to detect if someone is running mods. 52 | /// 53 | /// Our NML harmony instance 54 | /// Assemblies that were loaded when NML first started 55 | internal static void PatchNeos(Harmony harmony, HashSet initialAssemblies) 56 | { 57 | if (ModLoaderConfiguration.Get().HideModTypes) 58 | { 59 | // initialize the static assembly sets that our patches will need later 60 | neosAssemblies = GetNeosAssemblies(initialAssemblies); 61 | modAssemblies = GetModAssemblies(neosAssemblies); 62 | dotNetAssemblies = neosAssemblies.Where(LooksLikeDotNetAssembly).ToHashSet(); 63 | 64 | // TypeHelper.FindType explicitly does a type search 65 | MethodInfo findTypeTarget = AccessTools.DeclaredMethod(typeof(TypeHelper), nameof(TypeHelper.FindType), new Type[] { typeof(string) }); 66 | MethodInfo findTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(FindTypePostfix)); 67 | harmony.Patch(findTypeTarget, postfix: new HarmonyMethod(findTypePatch)); 68 | 69 | // WorkerManager.IsValidGenericType checks a type for validity, and if it returns `true` it reveals that the type exists 70 | MethodInfo isValidGenericTypeTarget = AccessTools.DeclaredMethod(typeof(WorkerManager), nameof(WorkerManager.IsValidGenericType), new Type[] { typeof(Type), typeof(bool) }); 71 | MethodInfo isValidGenericTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(IsValidTypePostfix)); 72 | harmony.Patch(isValidGenericTypeTarget, postfix: new HarmonyMethod(isValidGenericTypePatch)); 73 | 74 | // WorkerManager.GetType uses FindType, but upon failure fails back to doing a (strangely) exhausitive reflection-based search for the type 75 | MethodInfo getTypeTarget = AccessTools.DeclaredMethod(typeof(WorkerManager), nameof(WorkerManager.GetType), new Type[] { typeof(string) }); 76 | MethodInfo getTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(FindTypePostfix)); 77 | harmony.Patch(getTypeTarget, postfix: new HarmonyMethod(getTypePatch)); 78 | 79 | // FrooxEngine likes to enumerate all types in all assemblies, which is prone to issues (such as crashing FrooxCode if a type isn't loadable) 80 | MethodInfo getAssembliesTarget = AccessTools.DeclaredMethod(typeof(AppDomain), nameof(AppDomain.GetAssemblies), new Type[] { }); 81 | MethodInfo getAssembliesPatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(GetAssembliesPostfix)); 82 | harmony.Patch(getAssembliesTarget, postfix: new HarmonyMethod(getAssembliesPatch)); 83 | } 84 | } 85 | 86 | private static HashSet GetNeosAssemblies(HashSet initialAssemblies) 87 | { 88 | // Remove NML itself, as its types should be hidden but it's guaranteed to be loaded. 89 | initialAssemblies.Remove(Assembly.GetExecutingAssembly()); 90 | 91 | // Remove Harmony, as users who aren't using nml_libs will already have it loaded. 92 | initialAssemblies.Remove(typeof(Harmony).Assembly); 93 | 94 | return initialAssemblies; 95 | } 96 | 97 | private static HashSet GetModAssemblies(HashSet neosAssemblies) 98 | { 99 | // start with ALL assemblies 100 | HashSet assemblies = AppDomain.CurrentDomain.GetAssemblies().ToHashSet(); 101 | 102 | // remove assemblies that we know to have come with Neos 103 | assemblies.ExceptWith(neosAssemblies); 104 | 105 | // what's left are assemblies that magically appeared during the mod loading process. So mods and their dependencies. 106 | return assemblies; 107 | } 108 | 109 | /// 110 | /// Checks if an belongs to a mod or not. 111 | /// 112 | /// The to check. 113 | /// Type of root check being performed. Should be "type" or "assembly". Used in logging. 114 | /// Name of the root check being performed. Used in logging. 115 | /// If `true`, this will emit logs. If `false`, this function will not log. 116 | /// If `true`, then this function will always return `false` for late-loaded types 117 | /// `true` if this assembly belongs to a mod. 118 | private static bool IsModAssembly(Assembly assembly, string typeOrAssembly, string name, bool log, bool forceShowLate) 119 | { 120 | if (neosAssemblies!.Contains(assembly)) 121 | { 122 | // the type belongs to a Neos assembly 123 | return false; // don't hide the thing 124 | } 125 | else 126 | { 127 | if (modAssemblies!.Contains(assembly)) 128 | { 129 | // known type from a mod assembly 130 | if (log) 131 | { 132 | Logger.DebugFuncInternal(() => $"Hid {typeOrAssembly} \"{name}\" from Neos"); 133 | } 134 | return true; // hide the thing 135 | } 136 | else 137 | { 138 | // an assembly was in neither neosAssemblies nor modAssemblies 139 | // this implies someone late-loaded an assembly after NML, and it was later used in-game 140 | // this is super weird, and probably shouldn't ever happen... but if it does, I want to know about it. 141 | // since this is an edge case users may want to handle in different ways, the HideLateTypes nml config option allows them to choose. 142 | bool hideLate = ModLoaderConfiguration.Get().HideLateTypes; 143 | if (log) 144 | { 145 | Logger.WarnInternal($"The \"{name}\" {typeOrAssembly} does not appear to part of Neos or a mod. It is unclear whether it should be hidden or not. Due to the HideLateTypes config option being {hideLate} it will be {(hideLate ? "Hidden" : "Shown")}"); 146 | } 147 | // if forceShowLate == true, then this function will always return `false` for late-loaded types 148 | // if forceShowLate == false, then this function will return `true` when hideLate == true 149 | return hideLate && !forceShowLate; 150 | } 151 | } 152 | } 153 | 154 | /// 155 | /// Checks if an belongs to a mod or not. 156 | /// 157 | /// The to check 158 | /// If true, then this function will always return false for late-loaded types. 159 | /// true if this belongs to a mod. 160 | private static bool IsModAssembly(Assembly assembly, bool forceShowLate = false) 161 | { 162 | // this generates a lot of logspam, as a single call to AppDomain.GetAssemblies() calls this many times 163 | return IsModAssembly(assembly, "assembly", assembly.ToString(), log: false, forceShowLate); 164 | } 165 | 166 | /// 167 | /// Checks if a belongs to a mod or not. 168 | /// 169 | /// The to check. 170 | /// true if this belongs to a mod. 171 | private static bool IsModType(Type type) 172 | { 173 | return IsModAssembly(type.Assembly, "type", type.ToString(), log: true, forceShowLate: false); 174 | } 175 | 176 | // postfix for a method that searches for a type, and returns a reference to it if found (TypeHelper.FindType and WorkerManager.GetType) 177 | private static void FindTypePostfix(ref Type? __result) 178 | { 179 | if (__result != null) 180 | { 181 | // we only need to think about types if the method actually returned a non-null result 182 | if (IsModType(__result)) 183 | { 184 | __result = null; 185 | } 186 | } 187 | } 188 | 189 | // postfix for a method that validates a type (WorkerManager.IsValidGenericType) 190 | private static void IsValidTypePostfix(ref bool __result, Type type) 191 | { 192 | if (__result == true) 193 | { 194 | // we only need to think about types if the method actually returned a true result 195 | if (IsModType(type)) 196 | { 197 | __result = false; 198 | } 199 | } 200 | } 201 | 202 | private static void GetAssembliesPostfix(ref Assembly[] __result) 203 | { 204 | Assembly? callingAssembly = GetCallingAssembly(new(1)); 205 | if (callingAssembly != null && neosAssemblies!.Contains(callingAssembly)) 206 | { 207 | // if we're being called by Neos code, then hide mod assemblies 208 | Logger.DebugFuncInternal(() => $"Intercepting call to AppDomain.GetAssemblies() from {callingAssembly}"); 209 | __result = __result 210 | .Where(assembly => !IsModAssembly(assembly, forceShowLate: true)) // it turns out Neos itself late-loads a bunch of stuff, so we force-show late-loaded assemblies here 211 | .ToArray(); 212 | } 213 | } 214 | 215 | /// 216 | /// Get the calling using stack trace analysis, ignoring .NET assemblies. 217 | /// This implementation is SPECIFICALLY for the patch and may not be valid for other use-cases. 218 | /// 219 | /// The stack trace captured by the callee. 220 | /// The calling , or null if none was found. 221 | private static Assembly? GetCallingAssembly(StackTrace stackTrace) 222 | { 223 | for (int i = 0; i < stackTrace.FrameCount; i++) 224 | { 225 | Assembly? assembly = stackTrace.GetFrame(i)?.GetMethod()?.DeclaringType?.Assembly; 226 | // .NET calls AppDomain.GetAssemblies() a bunch internally, and we don't want to intercept those calls UNLESS they originated from Neos code. 227 | if (assembly != null && !dotNetAssemblies!.Contains(assembly)) 228 | { 229 | return assembly; 230 | } 231 | } 232 | return null; 233 | } 234 | 235 | private static bool LooksLikeDotNetAssembly(Assembly assembly) 236 | { 237 | // check the assembly's company 238 | string? company = assembly.GetCustomAttribute()?.Company; 239 | if (company != null && knownDotNetCompanies.Contains(company.ToLower())) 240 | { 241 | return true; 242 | } 243 | 244 | // check the assembly's product 245 | string? product = assembly.GetCustomAttribute()?.Product; 246 | if (product != null && knownDotNetProducts.Contains(product.ToLower())) 247 | { 248 | return true; 249 | } 250 | 251 | // nothing matched, this is probably not part of .NET 252 | return false; 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /NeosModLoader/AssemblyLoader.cs: -------------------------------------------------------------------------------- 1 | using NeosModLoader.Utility; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace NeosModLoader 9 | { 10 | internal static class AssemblyLoader 11 | { 12 | private static string[]? GetAssemblyPathsFromDir(string dirName) 13 | { 14 | string assembliesDirectory = Path.Combine(PlatformHelper.MainDirectory, dirName); 15 | 16 | Logger.MsgInternal($"loading assemblies from {dirName}"); 17 | 18 | string[]? assembliesToLoad = null; 19 | try 20 | { 21 | // Directory.GetFiles and Directory.EnumerateFiles have a fucked up API: https://learn.microsoft.com/en-us/dotnet/api/system.io.directory.getfiles?view=netframework-4.6.2#system-io-directory-getfiles(system-string-system-string-system-io-searchoption) 22 | // long story short if I searched for "*.dll" it would unhelpfully use some incredibly inconsistent behavior and return results like "foo.dll_disabled" 23 | // So I have to filter shit after the fact... ugh 24 | assembliesToLoad = Directory.EnumerateFiles(assembliesDirectory, "*.dll", SearchOption.AllDirectories) 25 | .Where(file => file.EndsWith(".dll")) 26 | .ToArray(); 27 | Array.Sort(assembliesToLoad, string.CompareOrdinal); 28 | } 29 | catch (Exception e) 30 | { 31 | if (e is DirectoryNotFoundException) 32 | { 33 | Logger.MsgInternal($"{dirName} directory not found, creating it now."); 34 | try 35 | { 36 | Directory.CreateDirectory(assembliesDirectory); 37 | } 38 | catch (Exception e2) 39 | { 40 | Logger.ErrorInternal($"Error creating ${dirName} directory:\n{e2}"); 41 | } 42 | } 43 | else 44 | { 45 | Logger.ErrorInternal($"Error enumerating ${dirName} directory:\n{e}"); 46 | } 47 | } 48 | return assembliesToLoad; 49 | } 50 | 51 | private static Assembly? LoadAssembly(string filepath) 52 | { 53 | string filename = Path.GetFileName(filepath); 54 | SplashChanger.SetCustom($"Loading file: {filename}"); 55 | Assembly assembly; 56 | try 57 | { 58 | Logger.DebugFuncInternal(() => $"load assembly {filename}"); 59 | assembly = Assembly.LoadFrom(filepath); 60 | } 61 | catch (Exception e) 62 | { 63 | Logger.ErrorInternal($"error loading assembly from {filepath}: {e}"); 64 | return null; 65 | } 66 | if (assembly == null) 67 | { 68 | Logger.ErrorInternal($"unexpected null loading assembly from {filepath}"); 69 | return null; 70 | } 71 | return assembly; 72 | } 73 | 74 | internal static AssemblyFile[] LoadAssembliesFromDir(string dirName) 75 | { 76 | List assemblyFiles = new(); 77 | if (GetAssemblyPathsFromDir(dirName) is string[] assemblyPaths) 78 | { 79 | foreach (string assemblyFilepath in assemblyPaths) 80 | { 81 | try 82 | { 83 | if (LoadAssembly(assemblyFilepath) is Assembly assembly) 84 | { 85 | assemblyFiles.Add(new AssemblyFile(assemblyFilepath, assembly)); 86 | } 87 | } 88 | catch (Exception e) 89 | { 90 | Logger.ErrorInternal($"Unexpected exception loading assembly from {assemblyFilepath}:\n{e}"); 91 | } 92 | } 93 | } 94 | return assemblyFiles.ToArray(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /NeosModLoader/AutoRegisterConfigKeyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NeosModLoader 4 | { 5 | /// 6 | /// Marks a field of type on a class 7 | /// deriving from to be automatically included in that mod's configuration. 8 | /// 9 | [AttributeUsage(AttributeTargets.Field)] 10 | public class AutoRegisterConfigKeyAttribute : Attribute 11 | { } 12 | } 13 | -------------------------------------------------------------------------------- /NeosModLoader/ConfigurationChangedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace NeosModLoader 2 | { 3 | /// 4 | /// Represents the data for the and events. 5 | /// 6 | public class ConfigurationChangedEvent 7 | { 8 | /// 9 | /// The in which the change occured. 10 | /// 11 | public ModConfiguration Config { get; private set; } 12 | 13 | /// 14 | /// The specific who's value changed. 15 | /// 16 | public ModConfigurationKey Key { get; private set; } 17 | 18 | /// 19 | /// A custom label that may be set by whoever changed the configuration. 20 | /// 21 | public string? Label { get; private set; } 22 | 23 | internal ConfigurationChangedEvent(ModConfiguration config, ModConfigurationKey key, string? label) 24 | { 25 | Config = config; 26 | Key = key; 27 | Label = label; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NeosModLoader/DebugInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.Versioning; 4 | 5 | namespace NeosModLoader 6 | { 7 | internal class DebugInfo 8 | { 9 | internal static void Log() 10 | { 11 | Logger.MsgInternal($"NeosModLoader v{ModLoader.VERSION} starting up!{(ModLoaderConfiguration.Get().Debug ? " Debug logs will be shown." : "")}"); 12 | Logger.MsgInternal($"CLR v{Environment.Version}"); 13 | Logger.DebugFuncInternal(() => $"Using .NET Framework: \"{AppDomain.CurrentDomain.SetupInformation.TargetFrameworkName}\""); 14 | Logger.DebugFuncInternal(() => $"Using .NET Core: \"{Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkName}\""); 15 | Logger.MsgInternal($"Using Harmony v{GetAssemblyVersion(typeof(HarmonyLib.Harmony))}"); 16 | Logger.MsgInternal($"Using BaseX v{GetAssemblyVersion(typeof(BaseX.floatQ))}"); 17 | Logger.MsgInternal($"Using FrooxEngine v{GetAssemblyVersion(typeof(FrooxEngine.IComponent))}"); 18 | Logger.MsgInternal($"Using Json.NET v{GetAssemblyVersion(typeof(Newtonsoft.Json.JsonSerializer))}"); 19 | } 20 | 21 | private static string? GetAssemblyVersion(Type typeFromAssembly) 22 | { 23 | return typeFromAssembly.Assembly.GetName()?.Version?.ToString(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NeosModLoader/DelegateExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace NeosModLoader 6 | { 7 | internal static class DelegateExtensions 8 | { 9 | internal static void SafeInvoke(this Delegate del, params object[] args) 10 | { 11 | var exceptions = new List(); 12 | 13 | foreach (var handler in del.GetInvocationList()) 14 | { 15 | try 16 | { 17 | handler.Method.Invoke(handler.Target, args); 18 | } 19 | catch (Exception ex) 20 | { 21 | exceptions.Add(ex); 22 | } 23 | } 24 | 25 | if (exceptions.Any()) 26 | { 27 | throw new AggregateException(exceptions); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /NeosModLoader/ExecutionHook.cs: -------------------------------------------------------------------------------- 1 | using FrooxEngine; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace NeosModLoader 8 | { 9 | [ImplementableClass(true)] 10 | internal class ExecutionHook 11 | { 12 | #pragma warning disable CS0169 13 | // fields must exist due to reflective access 14 | private static Type? __connectorType; // needed in all Neos versions 15 | private static Type? __connectorTypes; // needed in Neos 2021.10.17.1326 and later 16 | #pragma warning restore CS0169 17 | 18 | static ExecutionHook() 19 | { 20 | try 21 | { 22 | HashSet initialAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToHashSet(); 23 | SplashChanger.SetCustom("Loading libraries"); 24 | AssemblyFile[] loadedAssemblies = AssemblyLoader.LoadAssembliesFromDir("nml_libs"); 25 | // note that harmony may not be loaded until this point, so this class cannot directly inport HarmonyLib. 26 | 27 | if (loadedAssemblies.Length != 0) 28 | { 29 | string loadedAssemblyList = string.Join("\n", loadedAssemblies.Select(a => a.Assembly.FullName + " Sha256=" + a.Sha256)); 30 | Logger.MsgInternal($"Loaded libraries from nml_libs:\n{loadedAssemblyList}"); 31 | } 32 | 33 | SplashChanger.SetCustom("Initializing"); 34 | DebugInfo.Log(); 35 | NeosVersionReset.Initialize(); 36 | HarmonyWorker.LoadModsAndHideModAssemblies(initialAssemblies); 37 | SplashChanger.SetCustom("Loaded"); 38 | } 39 | catch (Exception e) // it's important that this doesn't send exceptions back to Neos 40 | { 41 | Logger.ErrorInternal($"Exception in execution hook!\n{e}"); 42 | } 43 | } 44 | 45 | // implementation not strictly required, but method must exist due to reflective access 46 | private static DummyConnector InstantiateConnector() 47 | { 48 | return new DummyConnector(); 49 | } 50 | 51 | // type must match return type of InstantiateConnector() 52 | private class DummyConnector : IConnector 53 | { 54 | public IImplementable? Owner { get; private set; } 55 | public void ApplyChanges() { } 56 | public void AssignOwner(IImplementable owner) => Owner = owner; 57 | public void Destroy(bool destroyingWorld) { } 58 | public void Initialize() { } 59 | public void RemoveOwner() => Owner = null; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /NeosModLoader/HarmonyWorker.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace NeosModLoader 6 | { 7 | // this class does all the harmony-related NML work. 8 | // this is needed to avoid importing harmony in ExecutionHook, where it may not be loaded yet. 9 | internal class HarmonyWorker 10 | { 11 | internal static void LoadModsAndHideModAssemblies(HashSet initialAssemblies) 12 | { 13 | Harmony harmony = new("com.neosmodloader"); 14 | ModLoader.LoadMods(harmony); 15 | AssemblyHider.PatchNeos(harmony, initialAssemblies); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NeosModLoader/IgnoresAccessChecksToAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace System.Runtime.CompilerServices 9 | { 10 | /// 11 | /// Makes the .NET runtime ignore access of private members of the with the given name.
12 | /// Use when building against publicized assemblies to prevent problems if Neos ever switches from running on Mono, 13 | /// where checking the "Allow Unsafe Code" option in the Project Settings is enough.
14 | /// 15 | /// Usage: [assembly: IgnoresAccessChecksTo("FrooxEngine")] 16 | ///
17 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 18 | public class IgnoresAccessChecksToAttribute : Attribute 19 | { 20 | /// 21 | /// Gets the name of the Assembly to ignore access checks to. 22 | /// 23 | public string AssemblyName { get; } 24 | 25 | /// 26 | /// Makes the .NET runtime ignore access of private members of the with the given name.
27 | /// Use when building against publicized assemblies to prevent problems if Neos ever switches from running on Mono, 28 | /// where checking the "Allow Unsafe Code" option in the Project Settings is enough.
29 | /// 30 | /// Usage: [assembly: IgnoresAccessChecksTo("FrooxEngine")] 31 | ///
32 | /// The name of the Assembly to ignore access checks to. 33 | public IgnoresAccessChecksToAttribute(string assemblyName) 34 | { 35 | AssemblyName = assemblyName; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NeosModLoader/JsonConverters/EnumConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace NeosModLoader.JsonConverters 5 | { 6 | // serializes and deserializes enums as strings 7 | internal class EnumConverter : JsonConverter 8 | { 9 | public override bool CanConvert(Type objectType) 10 | { 11 | return objectType.IsEnum; 12 | } 13 | 14 | public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 15 | { 16 | // handle old behavior where enums were serialized as underlying type 17 | Type underlyingType = Enum.GetUnderlyingType(objectType); 18 | if (TryConvert(reader!.Value!, underlyingType, out object? deserialized)) 19 | { 20 | Logger.DebugFuncInternal(() => $"Deserializing a BaseX type: {objectType} from a {reader!.Value!.GetType()}"); 21 | return deserialized!; 22 | } 23 | 24 | // handle new behavior where enums are serialized as strings 25 | if (reader.Value is string serialized) 26 | { 27 | return Enum.Parse(objectType, serialized); 28 | } 29 | 30 | throw new ArgumentException($"Could not deserialize a BaseX type: {objectType} from a {reader?.Value?.GetType()}. Expected underlying type was {underlyingType}"); 31 | } 32 | 33 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 34 | { 35 | string serialized = Enum.GetName(value!.GetType(), value); 36 | writer.WriteValue(serialized); 37 | } 38 | 39 | private bool TryConvert(object value, Type newType, out object? converted) 40 | { 41 | try 42 | { 43 | converted = Convert.ChangeType(value, newType); 44 | return true; 45 | } 46 | catch 47 | { 48 | converted = null; 49 | return false; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /NeosModLoader/JsonConverters/NeosPrimitiveConverter.cs: -------------------------------------------------------------------------------- 1 | using BaseX; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Reflection; 5 | 6 | namespace NeosModLoader.JsonConverters 7 | { 8 | internal class NeosPrimitiveConverter : JsonConverter 9 | { 10 | private static readonly Assembly BASEX = typeof(color).Assembly; 11 | 12 | public override bool CanConvert(Type objectType) 13 | { 14 | // handle all non-enum Neos Primitives in the BaseX assembly 15 | return !objectType.IsEnum && BASEX.Equals(objectType.Assembly) && Coder.IsNeosPrimitive(objectType); 16 | } 17 | 18 | public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) 19 | { 20 | if (reader.Value is string serialized) 21 | { 22 | // use Neos's built-in decoding if the value was serialized as a string 23 | return typeof(Coder<>).MakeGenericType(objectType).GetMethod("DecodeFromString").Invoke(null, new object[] { serialized }); 24 | } 25 | 26 | throw new ArgumentException($"Could not deserialize a BaseX type: {objectType} from a {reader?.Value?.GetType()}"); 27 | } 28 | 29 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) 30 | { 31 | string serialized = (string)typeof(Coder<>).MakeGenericType(value!.GetType()).GetMethod("EncodeToString").Invoke(null, new object[] { value }); 32 | writer.WriteValue(serialized); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NeosModLoader/LoadedNeosMod.cs: -------------------------------------------------------------------------------- 1 | namespace NeosModLoader 2 | { 3 | internal class LoadedNeosMod 4 | { 5 | internal LoadedNeosMod(NeosMod neosMod, AssemblyFile modAssembly) 6 | { 7 | NeosMod = neosMod; 8 | ModAssembly = modAssembly; 9 | } 10 | 11 | internal NeosMod NeosMod { get; private set; } 12 | internal AssemblyFile ModAssembly { get; private set; } 13 | internal ModConfiguration? ModConfiguration { get; set; } 14 | internal bool AllowSavingConfiguration = true; 15 | internal bool FinishedLoading { get => NeosMod.FinishedLoading; set => NeosMod.FinishedLoading = value; } 16 | internal string Name { get => NeosMod.Name; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /NeosModLoader/Logger.cs: -------------------------------------------------------------------------------- 1 | using BaseX; 2 | using System; 3 | using System.Diagnostics; 4 | 5 | namespace NeosModLoader 6 | { 7 | internal class Logger 8 | { 9 | // logged for null objects 10 | internal readonly static string NULL_STRING = "null"; 11 | 12 | internal static bool IsDebugEnabled() 13 | { 14 | return ModLoaderConfiguration.Get().Debug; 15 | } 16 | 17 | internal static void DebugFuncInternal(Func messageProducer) 18 | { 19 | if (IsDebugEnabled()) 20 | { 21 | LogInternal(LogType.DEBUG, messageProducer()); 22 | } 23 | } 24 | 25 | internal static void DebugFuncExternal(Func messageProducer) 26 | { 27 | if (IsDebugEnabled()) 28 | { 29 | LogInternal(LogType.DEBUG, messageProducer(), SourceFromStackTrace(new(1))); 30 | } 31 | } 32 | 33 | internal static void DebugInternal(string message) 34 | { 35 | if (IsDebugEnabled()) 36 | { 37 | LogInternal(LogType.DEBUG, message); 38 | } 39 | } 40 | 41 | internal static void DebugExternal(object message) 42 | { 43 | if (IsDebugEnabled()) 44 | { 45 | LogInternal(LogType.DEBUG, message, SourceFromStackTrace(new(1))); 46 | } 47 | } 48 | 49 | internal static void DebugListExternal(object[] messages) 50 | { 51 | if (IsDebugEnabled()) 52 | { 53 | LogListInternal(LogType.DEBUG, messages, SourceFromStackTrace(new(1))); 54 | } 55 | } 56 | 57 | internal static void MsgInternal(string message) => LogInternal(LogType.INFO, message); 58 | internal static void MsgExternal(object message) => LogInternal(LogType.INFO, message, SourceFromStackTrace(new(1))); 59 | internal static void MsgListExternal(object[] messages) => LogListInternal(LogType.INFO, messages, SourceFromStackTrace(new(1))); 60 | internal static void WarnInternal(string message) => LogInternal(LogType.WARN, message); 61 | internal static void WarnExternal(object message) => LogInternal(LogType.WARN, message, SourceFromStackTrace(new(1))); 62 | internal static void WarnListExternal(object[] messages) => LogListInternal(LogType.WARN, messages, SourceFromStackTrace(new(1))); 63 | internal static void ErrorInternal(string message) => LogInternal(LogType.ERROR, message); 64 | internal static void ErrorExternal(object message) => LogInternal(LogType.ERROR, message, SourceFromStackTrace(new(1))); 65 | internal static void ErrorListExternal(object[] messages) => LogListInternal(LogType.ERROR, messages, SourceFromStackTrace(new(1))); 66 | 67 | private static void LogInternal(string logTypePrefix, object message, string? source = null) 68 | { 69 | if (message == null) 70 | { 71 | message = NULL_STRING; 72 | } 73 | if (source == null) 74 | { 75 | UniLog.Log($"{logTypePrefix}[NeosModLoader] {message}"); 76 | } 77 | else 78 | { 79 | UniLog.Log($"{logTypePrefix}[NeosModLoader/{source}] {message}"); 80 | } 81 | } 82 | 83 | private static void LogListInternal(string logTypePrefix, object[] messages, string? source) 84 | { 85 | if (messages == null) 86 | { 87 | LogInternal(logTypePrefix, NULL_STRING, source); 88 | } 89 | else 90 | { 91 | foreach (object element in messages) 92 | { 93 | LogInternal(logTypePrefix, element.ToString(), source); 94 | } 95 | } 96 | } 97 | 98 | private static string? SourceFromStackTrace(StackTrace stackTrace) 99 | { 100 | // MsgExternal() and Msg() are above us in the stack 101 | return Util.ExecutingMod(stackTrace)?.Name; 102 | } 103 | 104 | private sealed class LogType 105 | { 106 | internal readonly static string DEBUG = "[DEBUG]"; 107 | internal readonly static string INFO = "[INFO] "; 108 | internal readonly static string WARN = "[WARN] "; 109 | internal readonly static string ERROR = "[ERROR]"; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /NeosModLoader/ModConfigurationDefinitionBuilder.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace NeosModLoader 8 | { 9 | /// 10 | /// Represents a fluent configuration interface to define mod configurations. 11 | /// 12 | public class ModConfigurationDefinitionBuilder 13 | { 14 | private readonly NeosModBase Owner; 15 | private Version ConfigVersion = new(1, 0, 0); 16 | private readonly HashSet Keys = new(); 17 | private bool AutoSaveConfig = true; 18 | 19 | internal ModConfigurationDefinitionBuilder(NeosModBase owner) 20 | { 21 | Owner = owner; 22 | } 23 | 24 | /// 25 | /// Sets the semantic version of this configuration definition. Default is 1.0.0. 26 | /// 27 | /// The config's semantic version. 28 | /// This builder. 29 | public ModConfigurationDefinitionBuilder Version(Version version) 30 | { 31 | ConfigVersion = version; 32 | return this; 33 | } 34 | 35 | /// 36 | /// Sets the semantic version of this configuration definition. Default is 1.0.0. 37 | /// 38 | /// The config's semantic version, as a string. 39 | /// This builder. 40 | public ModConfigurationDefinitionBuilder Version(string version) 41 | { 42 | ConfigVersion = new Version(version); 43 | return this; 44 | } 45 | 46 | /// 47 | /// Adds a new key to this configuration definition. 48 | /// 49 | /// A configuration key. 50 | /// This builder. 51 | public ModConfigurationDefinitionBuilder Key(ModConfigurationKey key) 52 | { 53 | Keys.Add(key); 54 | return this; 55 | } 56 | 57 | /// 58 | /// Sets the AutoSave property of this configuration definition. Default is true. 59 | /// 60 | /// If false, the config will not be autosaved on Neos close. 61 | /// This builder. 62 | public ModConfigurationDefinitionBuilder AutoSave(bool autoSave) 63 | { 64 | AutoSaveConfig = autoSave; 65 | return this; 66 | } 67 | 68 | internal void ProcessAttributes() 69 | { 70 | var fields = AccessTools.GetDeclaredFields(Owner.GetType()); 71 | fields 72 | .Where(field => Attribute.GetCustomAttribute(field, typeof(AutoRegisterConfigKeyAttribute)) != null) 73 | .Do(ProcessField); 74 | } 75 | 76 | private void ProcessField(FieldInfo field) 77 | { 78 | if (!typeof(ModConfigurationKey).IsAssignableFrom(field.FieldType)) 79 | { 80 | // wrong type 81 | Logger.WarnInternal($"{Owner.Name} had an [AutoRegisterConfigKey] field of the wrong type: {field}"); 82 | return; 83 | } 84 | 85 | ModConfigurationKey fieldValue = (ModConfigurationKey)field.GetValue(field.IsStatic ? null : Owner); 86 | Keys.Add(fieldValue); 87 | } 88 | 89 | internal ModConfigurationDefinition? Build() 90 | { 91 | if (Keys.Count > 0) 92 | { 93 | return new ModConfigurationDefinition(Owner, ConfigVersion, Keys, AutoSaveConfig); 94 | } 95 | return null; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /NeosModLoader/ModConfigurationKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NeosModLoader 5 | { 6 | /// 7 | /// Represents an untyped mod configuration key. 8 | /// 9 | public abstract class ModConfigurationKey 10 | { 11 | internal ModConfigurationKey(string name, string? description, bool internalAccessOnly) 12 | { 13 | Name = name ?? throw new ArgumentNullException("Configuration key name must not be null"); 14 | Description = description; 15 | InternalAccessOnly = internalAccessOnly; 16 | } 17 | 18 | /// 19 | /// Gets the mod-unique name of this config item. Must be present. 20 | /// 21 | public string Name { get; private set; } 22 | 23 | /// 24 | /// Gets the human-readable description of this config item. Should be specified by the defining mod. 25 | /// 26 | public string? Description { get; private set; } 27 | 28 | /// 29 | /// Gets whether only the owning mod should have access to this config item. 30 | /// 31 | public bool InternalAccessOnly { get; private set; } 32 | 33 | /// 34 | /// Get the of this key's value. 35 | /// 36 | /// The of this key's value. 37 | public abstract Type ValueType(); 38 | 39 | /// 40 | /// Checks if a value is valid for this configuration item. 41 | /// 42 | /// The value to check. 43 | /// true if the value is valid. 44 | public abstract bool Validate(object? value); 45 | 46 | /// 47 | /// Tries to compute the default value for this key, if a default provider was set. 48 | /// 49 | /// The computed default value if the return value is true. Otherwise default. 50 | /// true if the default value was successfully computed. 51 | public abstract bool TryComputeDefault(out object? defaultValue); 52 | 53 | /// 54 | /// We only care about key name for non-defining keys.
55 | /// For defining keys all of the other properties (default, validator, etc.) also matter. 56 | ///
57 | /// The other object to compare against. 58 | /// true if the other object is equal to this. 59 | public override bool Equals(object obj) 60 | { 61 | return obj is ModConfigurationKey key && 62 | Name == key.Name; 63 | } 64 | 65 | /// 66 | public override int GetHashCode() 67 | { 68 | return 539060726 + EqualityComparer.Default.GetHashCode(Name); 69 | } 70 | 71 | private object? Value; 72 | internal bool HasValue; 73 | 74 | /// 75 | /// Each configuration item has exactly ONE defining key, and that is the key defined by the mod. 76 | /// Duplicate keys can be created (they only need to share the same Name) and they'll still work 77 | /// for reading configs. 78 | /// 79 | /// This is a non-null self-reference for the defining key itself as soon as the definition is done initializing. 80 | /// 81 | internal ModConfigurationKey? DefiningKey; 82 | 83 | internal bool TryGetValue(out object? value) 84 | { 85 | if (HasValue) 86 | { 87 | value = Value; 88 | return true; 89 | } 90 | else 91 | { 92 | value = null; 93 | return false; 94 | } 95 | } 96 | 97 | internal void Set(object? value) 98 | { 99 | Value = value; 100 | HasValue = true; 101 | } 102 | 103 | internal bool Unset() 104 | { 105 | bool hadValue = HasValue; 106 | HasValue = false; 107 | return hadValue; 108 | } 109 | } 110 | 111 | /// 112 | /// Represents a typed mod configuration key. 113 | /// 114 | /// The type of this key's value. 115 | public class ModConfigurationKey : ModConfigurationKey 116 | { 117 | /// 118 | /// Creates a new instance of the class with the given parameters. 119 | /// 120 | /// The mod-unique name of this config item. 121 | /// The human-readable description of this config item. 122 | /// The function that computes a default value for this key. Otherwise default() will be used. 123 | /// If true, only the owning mod should have access to this config item. 124 | /// The function that checks if the given value is valid for this configuration item. Otherwise everything will be accepted. 125 | public ModConfigurationKey(string name, string? description = null, Func? computeDefault = null, bool internalAccessOnly = false, Predicate? valueValidator = null) : base(name, description, internalAccessOnly) 126 | { 127 | ComputeDefault = computeDefault; 128 | IsValueValid = valueValidator; 129 | } 130 | 131 | private readonly Func? ComputeDefault; 132 | private readonly Predicate? IsValueValid; 133 | 134 | /// 135 | public override Type ValueType() => typeof(T); 136 | 137 | /// 138 | public override bool Validate(object? value) 139 | { 140 | if (value is T typedValue) 141 | { 142 | // value is of the correct type 143 | return ValidateTyped(typedValue); 144 | } 145 | else if (value == null) 146 | { 147 | if (Util.CanBeNull(ValueType())) 148 | { 149 | // null is valid for T 150 | return ValidateTyped((T?)value); 151 | } 152 | else 153 | { 154 | // null is not valid for T 155 | return false; 156 | } 157 | } 158 | else 159 | { 160 | // value is of the wrong type 161 | return false; 162 | } 163 | } 164 | 165 | /// 166 | /// Checks if a value is valid for this configuration item. 167 | /// 168 | /// The value to check. 169 | /// true if the value is valid. 170 | public bool ValidateTyped(T? value) 171 | { 172 | if (IsValueValid == null) 173 | { 174 | return true; 175 | } 176 | else 177 | { 178 | return IsValueValid(value); 179 | } 180 | } 181 | 182 | /// 183 | public override bool TryComputeDefault(out object? defaultValue) 184 | { 185 | if (TryComputeDefaultTyped(out T? defaultTypedValue)) 186 | { 187 | defaultValue = defaultTypedValue; 188 | return true; 189 | } 190 | else 191 | { 192 | defaultValue = null; 193 | return false; 194 | } 195 | } 196 | 197 | /// 198 | /// Tries to compute the default value for this key, if a default provider was set. 199 | /// 200 | /// The computed default value if the return value is true. Otherwise default(T). 201 | /// true if the default value was successfully computed. 202 | public bool TryComputeDefaultTyped(out T? defaultValue) 203 | { 204 | if (ComputeDefault == null) 205 | { 206 | defaultValue = default; 207 | return false; 208 | } 209 | else 210 | { 211 | defaultValue = ComputeDefault(); 212 | return true; 213 | } 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /NeosModLoader/ModLoader.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | 9 | namespace NeosModLoader 10 | { 11 | /// 12 | /// Contains the actual mod loader. 13 | /// 14 | public class ModLoader 15 | { 16 | internal const string VERSION_CONSTANT = "1.12.6"; 17 | /// 18 | /// NeosModLoader's version 19 | /// 20 | public static readonly string VERSION = VERSION_CONSTANT; 21 | private static readonly Type NEOS_MOD_TYPE = typeof(NeosMod); 22 | private static readonly List LoadedMods = new(); // used for mod enumeration 23 | internal static readonly Dictionary AssemblyLookupMap = new(); // used for logging 24 | private static readonly Dictionary ModNameLookupMap = new(); // used for duplicate mod checking 25 | 26 | /// 27 | /// Allows reading metadata for all loaded mods 28 | /// 29 | /// A new list containing each loaded mod 30 | public static IEnumerable Mods() 31 | { 32 | return LoadedMods 33 | .Select(m => (NeosModBase)m.NeosMod) 34 | .ToList(); 35 | } 36 | 37 | internal static void LoadMods(Harmony harmony) 38 | { 39 | ModLoaderConfiguration config = ModLoaderConfiguration.Get(); 40 | if (config.NoMods) 41 | { 42 | Logger.DebugInternal("mods will not be loaded due to configuration file"); 43 | return; 44 | } 45 | SplashChanger.SetCustom("Looking for mods"); 46 | 47 | // generate list of assemblies to load 48 | AssemblyFile[] modsToLoad; 49 | if (AssemblyLoader.LoadAssembliesFromDir("nml_mods") is AssemblyFile[] arr) 50 | { 51 | modsToLoad = arr; 52 | } 53 | else 54 | { 55 | return; 56 | } 57 | 58 | ModConfiguration.EnsureDirectoryExists(); 59 | 60 | // call Initialize() each mod 61 | foreach (AssemblyFile mod in modsToLoad) 62 | { 63 | try 64 | { 65 | LoadedNeosMod? loaded = InitializeMod(mod); 66 | if (loaded != null) 67 | { 68 | // if loading succeeded, then we need to register the mod 69 | RegisterMod(loaded); 70 | } 71 | } 72 | catch (ReflectionTypeLoadException reflectionTypeLoadException) 73 | { 74 | // this exception type has some inner exceptions we must also log to gain any insight into what went wrong 75 | StringBuilder sb = new(); 76 | sb.AppendLine(reflectionTypeLoadException.ToString()); 77 | foreach (Exception loaderException in reflectionTypeLoadException.LoaderExceptions) 78 | { 79 | sb.AppendLine($"Loader Exception: {loaderException.Message}"); 80 | if (loaderException is FileNotFoundException fileNotFoundException) 81 | { 82 | if (!string.IsNullOrEmpty(fileNotFoundException.FusionLog)) 83 | { 84 | sb.Append(" Fusion Log:\n "); 85 | sb.AppendLine(fileNotFoundException.FusionLog); 86 | } 87 | } 88 | } 89 | Logger.ErrorInternal($"ReflectionTypeLoadException initializing mod from {mod.File}:\n{sb}"); 90 | } 91 | catch (Exception e) 92 | { 93 | Logger.ErrorInternal($"Unexpected exception initializing mod from {mod.File}:\n{e}"); 94 | } 95 | } 96 | 97 | SplashChanger.SetCustom("Hooking big fish"); 98 | ModConfiguration.RegisterShutdownHook(harmony); 99 | 100 | foreach (LoadedNeosMod mod in LoadedMods) 101 | { 102 | try 103 | { 104 | HookMod(mod); 105 | } 106 | catch (Exception e) 107 | { 108 | Logger.ErrorInternal($"Unexpected exception in OnEngineInit() for mod {mod.NeosMod.Name} from {mod.ModAssembly.File}:\n{e}"); 109 | } 110 | } 111 | 112 | // log potential conflicts 113 | if (config.LogConflicts) 114 | { 115 | SplashChanger.SetCustom("Looking for conflicts"); 116 | 117 | IEnumerable patchedMethods = Harmony.GetAllPatchedMethods(); 118 | foreach (MethodBase patchedMethod in patchedMethods) 119 | { 120 | Patches patches = Harmony.GetPatchInfo(patchedMethod); 121 | HashSet owners = new(patches.Owners); 122 | if (owners.Count > 1) 123 | { 124 | Logger.WarnInternal($"method \"{patchedMethod.FullDescription()}\" has been patched by the following:"); 125 | foreach (string owner in owners) 126 | { 127 | Logger.WarnInternal($" \"{owner}\" ({TypesForOwner(patches, owner)})"); 128 | } 129 | } 130 | else if (config.Debug) 131 | { 132 | string owner = owners.FirstOrDefault(); 133 | Logger.DebugFuncInternal(() => $"method \"{patchedMethod.FullDescription()}\" has been patched by \"{owner}\""); 134 | } 135 | } 136 | } 137 | } 138 | 139 | /// 140 | /// We have a bunch of maps and things the mod needs to be registered in. This method does all that jazz. 141 | /// 142 | /// The successfully loaded mod to register 143 | private static void RegisterMod(LoadedNeosMod mod) 144 | { 145 | try 146 | { 147 | ModNameLookupMap.Add(mod.NeosMod.Name, mod); 148 | } 149 | catch (ArgumentException) 150 | { 151 | LoadedNeosMod existing = ModNameLookupMap[mod.NeosMod.Name]; 152 | Logger.ErrorInternal($"{mod.ModAssembly.File} declares duplicate mod {mod.NeosMod.Name} already declared in {existing.ModAssembly.File}. The new mod will be ignored."); 153 | return; 154 | } 155 | 156 | LoadedMods.Add(mod); 157 | AssemblyLookupMap.Add(mod.ModAssembly.Assembly, mod.NeosMod); 158 | mod.NeosMod.loadedNeosMod = mod; // complete the circular reference (used to look up config) 159 | mod.FinishedLoading = true; // used to signal that the mod is truly loaded 160 | } 161 | 162 | private static string TypesForOwner(Patches patches, string owner) 163 | { 164 | bool ownerEquals(Patch patch) => Equals(patch.owner, owner); 165 | int prefixCount = patches.Prefixes.Where(ownerEquals).Count(); 166 | int postfixCount = patches.Postfixes.Where(ownerEquals).Count(); 167 | int transpilerCount = patches.Transpilers.Where(ownerEquals).Count(); 168 | int finalizerCount = patches.Finalizers.Where(ownerEquals).Count(); 169 | return $"prefix={prefixCount}; postfix={postfixCount}; transpiler={transpilerCount}; finalizer={finalizerCount}"; 170 | } 171 | 172 | // loads mod class and mod config 173 | private static LoadedNeosMod? InitializeMod(AssemblyFile mod) 174 | { 175 | if (mod.Assembly == null) 176 | { 177 | return null; 178 | } 179 | 180 | Type[] modClasses = mod.Assembly.GetLoadableTypes(t => t.IsClass && !t.IsAbstract && NEOS_MOD_TYPE.IsAssignableFrom(t)).ToArray(); 181 | if (modClasses.Length == 0) 182 | { 183 | Logger.ErrorInternal($"no mods found in {mod.File}"); 184 | return null; 185 | } 186 | else if (modClasses.Length != 1) 187 | { 188 | Logger.ErrorInternal($"more than one mod found in {mod.File}. no mods will be loaded."); 189 | return null; 190 | } 191 | else 192 | { 193 | Type modClass = modClasses[0]; 194 | NeosMod? neosMod = null; 195 | try 196 | { 197 | neosMod = (NeosMod)AccessTools.CreateInstance(modClass); 198 | } 199 | catch (Exception e) 200 | { 201 | Logger.ErrorInternal($"error instantiating mod {modClass.FullName} from {mod.File}:\n{e}"); 202 | return null; 203 | } 204 | if (neosMod == null) 205 | { 206 | Logger.ErrorInternal($"unexpected null instantiating mod {modClass.FullName} from {mod.File}"); 207 | return null; 208 | } 209 | SplashChanger.SetCustom($"Loading configuration for [{neosMod.Name}/{neosMod.Version}]"); 210 | 211 | LoadedNeosMod loadedMod = new(neosMod, mod); 212 | Logger.MsgInternal($"loaded mod [{neosMod.Name}/{neosMod.Version}] ({Path.GetFileName(mod.File)}) by {neosMod.Author} with 256hash: {mod.Sha256}"); 213 | loadedMod.ModConfiguration = ModConfiguration.LoadConfigForMod(loadedMod); 214 | return loadedMod; 215 | } 216 | } 217 | 218 | private static void HookMod(LoadedNeosMod mod) 219 | { 220 | SplashChanger.SetCustom($"Starting mod [{mod.NeosMod.Name}/{mod.NeosMod.Version}]"); 221 | Logger.DebugFuncInternal(() => $"calling OnEngineInit() for [{mod.NeosMod.Name}]"); 222 | try 223 | { 224 | mod.NeosMod.OnEngineInit(); 225 | } 226 | catch (Exception e) 227 | { 228 | Logger.ErrorInternal($"mod {mod.NeosMod.Name} from {mod.ModAssembly.File} threw error from OnEngineInit():\n{e}"); 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /NeosModLoader/ModLoaderConfiguration.cs: -------------------------------------------------------------------------------- 1 | using NeosModLoader.Utility; 2 | using System; 3 | using System.IO; 4 | using System.Reflection; 5 | 6 | namespace NeosModLoader 7 | { 8 | internal class ModLoaderConfiguration 9 | { 10 | private static readonly string CONFIG_FILENAME = "NeosModLoader.config"; 11 | 12 | private static ModLoaderConfiguration? _configuration; 13 | 14 | internal static ModLoaderConfiguration Get() 15 | { 16 | if (_configuration == null) 17 | { 18 | // the config file can just sit next to the dll. Simple. 19 | // ...unless the DLL is embedded. In that case, fallback to MainDirectory. 20 | string path = Path.Combine(GetAssemblyDirectory(), CONFIG_FILENAME); 21 | _configuration = new ModLoaderConfiguration(); 22 | 23 | // .NET's ConfigurationManager is some hot trash to the point where I'm just done with it. 24 | // Time to reinvent the wheel. This parses simple key=value style properties from a text file. 25 | try 26 | { 27 | var lines = File.ReadAllLines(path); 28 | foreach (var line in lines) 29 | { 30 | int splitIdx = line.IndexOf('='); 31 | if (splitIdx != -1) 32 | { 33 | string key = line.Substring(0, splitIdx); 34 | string value = line.Substring(splitIdx + 1); 35 | 36 | if ("unsafe".Equals(key) && "true".Equals(value)) 37 | { 38 | _configuration.Unsafe = true; 39 | } 40 | else if ("debug".Equals(key) && "true".Equals(value)) 41 | { 42 | _configuration.Debug = true; 43 | } 44 | else if ("hidevisuals".Equals(key) && "true".Equals(value)) 45 | { 46 | _configuration.HideVisuals = true; 47 | } 48 | else if ("nomods".Equals(key) && "true".Equals(value)) 49 | { 50 | _configuration.NoMods = true; 51 | } 52 | else if ("nolibraries".Equals(key) && "true".Equals(value)) 53 | { 54 | _configuration.NoLibraries = true; 55 | } 56 | else if ("advertiseversion".Equals(key) && "true".Equals(value)) 57 | { 58 | _configuration.AdvertiseVersion = true; 59 | } 60 | else if ("logconflicts".Equals(key) && "false".Equals(value)) 61 | { 62 | _configuration.LogConflicts = false; 63 | } 64 | else if ("hidemodtypes".Equals(key) && "false".Equals(value)) 65 | { 66 | _configuration.HideModTypes = false; 67 | } 68 | else if ("hidelatetypes".Equals(key) && "false".Equals(value)) 69 | { 70 | _configuration.HideLateTypes = false; 71 | } 72 | } 73 | } 74 | } 75 | catch (Exception e) 76 | { 77 | if (e is FileNotFoundException) 78 | { 79 | Logger.MsgInternal($"{path} is missing! This is probably fine."); 80 | } 81 | else if (e is DirectoryNotFoundException || e is IOException || e is UnauthorizedAccessException) 82 | { 83 | Logger.WarnInternal(e.ToString()); 84 | } 85 | else 86 | { 87 | throw; 88 | } 89 | } 90 | } 91 | return _configuration; 92 | } 93 | 94 | private static string GetAssemblyDirectory() 95 | { 96 | string codeBase = Assembly.GetExecutingAssembly().CodeBase; 97 | UriBuilder uri = new(codeBase); 98 | string path = Uri.UnescapeDataString(uri.Path); 99 | if (PlatformHelper.IsPathEmbedded(path)) return PlatformHelper.MainDirectory; 100 | return Path.GetDirectoryName(path); 101 | } 102 | 103 | public bool Unsafe { get; private set; } = false; 104 | public bool Debug { get; private set; } = false; 105 | public bool HideVisuals { get; private set; } = false; 106 | public bool NoMods { get; private set; } = false; 107 | public bool NoLibraries { get; private set; } = false; 108 | public bool AdvertiseVersion { get; private set; } = false; 109 | public bool LogConflicts { get; private set; } = true; 110 | public bool HideModTypes { get; private set; } = true; 111 | public bool HideLateTypes { get; private set; } = true; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /NeosModLoader/NeosMod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NeosModLoader 5 | { 6 | /// 7 | /// Contains members that only the or the Mod itself are intended to access. 8 | /// 9 | public abstract class NeosMod : NeosModBase 10 | { 11 | /// 12 | /// Gets whether debug logging is enabled. 13 | /// 14 | /// true if debug logging is enabled. 15 | public static bool IsDebugEnabled() => Logger.IsDebugEnabled(); 16 | 17 | /// 18 | /// Logs an object as a line in the log based on the value produced by the given function if debug logging is enabled.. 19 | /// 20 | /// This is more efficient than passing an or a directly, 21 | /// as it won't be generated if debug logging is disabled. 22 | /// 23 | /// The function generating the object to log. 24 | public static void DebugFunc(Func messageProducer) => Logger.DebugFuncExternal(messageProducer); 25 | 26 | /// 27 | /// Logs the given message as a line in the log if debug logging is enabled. Prefer or . 28 | /// 29 | /// The message to log. 30 | public static void Debug(string message) => Logger.DebugExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) 31 | 32 | /// 33 | /// Logs the given object as a line in the log if debug logging is enabled. 34 | /// 35 | /// The object to log. 36 | public static void Debug(object message) => Logger.DebugExternal(message); 37 | 38 | /// 39 | /// Logs the given objects as lines in the log if debug logging is enabled. 40 | /// 41 | /// The objects to log. 42 | public static void Debug(params object[] messages) => Logger.DebugListExternal(messages); 43 | 44 | 45 | /// 46 | /// Logs the given message as a regular line in the log. Prefer . 47 | /// 48 | /// The message to log. 49 | public static void Msg(string message) => Logger.MsgExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) 50 | 51 | /// 52 | /// Logs the given object as a regular line in the log. 53 | /// 54 | /// The object to log. 55 | public static void Msg(object message) => Logger.MsgExternal(message); 56 | 57 | /// 58 | /// Logs the given objects as regular lines in the log. 59 | /// 60 | /// The objects to log. 61 | public static void Msg(params object[] messages) => Logger.MsgListExternal(messages); 62 | 63 | 64 | /// 65 | /// Logs the given message as a warning line in the log. Prefer . 66 | /// 67 | /// The message to log. 68 | public static void Warn(string message) => Logger.WarnExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) 69 | 70 | /// 71 | /// Logs the given object as a warning line in the log. 72 | /// 73 | /// The object to log. 74 | public static void Warn(object message) => Logger.WarnExternal(message); 75 | 76 | /// 77 | /// Logs the given objects as warning lines in the log. 78 | /// 79 | /// The objects to log. 80 | public static void Warn(params object[] messages) => Logger.WarnListExternal(messages); 81 | 82 | 83 | /// 84 | /// Logs the given message as an error line in the log. Prefer . 85 | /// 86 | /// The message to log. 87 | public static void Error(string message) => Logger.ErrorExternal(message); // needed for binary compatibility (REMOVE IN NEXT MAJOR VERSION) 88 | 89 | /// 90 | /// Logs the given object as an error line in the log. 91 | /// 92 | /// The object to log. 93 | public static void Error(object message) => Logger.ErrorExternal(message); 94 | 95 | /// 96 | /// Logs the given objects as error lines in the log. 97 | /// 98 | /// The objects to log. 99 | public static void Error(params object[] messages) => Logger.ErrorListExternal(messages); 100 | 101 | /// 102 | /// Called once immediately after NeosModLoader begins execution 103 | /// 104 | public virtual void OnEngineInit() { } 105 | 106 | /// 107 | /// Build the defined configuration for this mod. 108 | /// 109 | /// This mod's configuration definition. 110 | internal ModConfigurationDefinition? BuildConfigurationDefinition() 111 | { 112 | ModConfigurationDefinitionBuilder builder = new(this); 113 | builder.ProcessAttributes(); 114 | DefineConfiguration(builder); 115 | return builder.Build(); 116 | } 117 | 118 | /// 119 | /// Get the defined configuration for this mod. This should be overridden by your mod if necessary. 120 | /// 121 | /// This mod's configuration definition. calls DefineConfiguration(ModConfigurationDefinitionBuilder) by default. 122 | [Obsolete("This method is obsolete. Use DefineConfiguration(ModConfigurationDefinitionBuilder) instead.")] // REMOVE IN NEXT MAJOR VERSION 123 | public virtual ModConfigurationDefinition? GetConfigurationDefinition() 124 | { 125 | return BuildConfigurationDefinition(); 126 | } 127 | 128 | /// 129 | /// Create a configuration definition for this mod. 130 | /// 131 | /// The semantic version of the configuration definition 132 | /// A list of configuration items 133 | /// 134 | [Obsolete("This method is obsolete. Use DefineConfiguration(ModConfigurationDefinitionBuilder) instead.")] // REMOVE IN NEXT MAJOR VERSION 135 | public ModConfigurationDefinition DefineConfiguration(Version version, IEnumerable configurationItemDefinitions) // needed for binary compatibility 136 | { 137 | return DefineConfiguration(version, configurationItemDefinitions, true); 138 | } 139 | 140 | /// 141 | /// Create a configuration definition for this mod. 142 | /// 143 | /// The semantic version of the configuration definition 144 | /// A list of configuration items 145 | /// If false, the config will not be autosaved on Neos close 146 | /// 147 | [Obsolete("This method is obsolete. Use DefineConfiguration(ModConfigurationDefinitionBuilder) instead.")] // REMOVE IN NEXT MAJOR VERSION 148 | public ModConfigurationDefinition DefineConfiguration(Version version, IEnumerable configurationItemDefinitions, bool autoSave = true) 149 | { 150 | if (version == null) 151 | { 152 | throw new ArgumentNullException("version must be non-null"); 153 | } 154 | 155 | if (configurationItemDefinitions == null) 156 | { 157 | throw new ArgumentNullException("configurationItemDefinitions must be non-null"); 158 | } 159 | 160 | return new ModConfigurationDefinition(this, version, new HashSet(configurationItemDefinitions), autoSave); 161 | } 162 | 163 | /// 164 | /// Define this mod's configuration via a builder 165 | /// 166 | /// A builder you can use to define the mod's configuration 167 | public virtual void DefineConfiguration(ModConfigurationDefinitionBuilder builder) 168 | { 169 | } 170 | 171 | /// 172 | /// Defines handling of incompatible configuration versions 173 | /// 174 | /// Configuration version read from the config file 175 | /// Configuration version defined in the mod code 176 | /// 177 | public virtual IncompatibleConfigurationHandlingOption HandleIncompatibleConfigurationVersions(Version serializedVersion, Version definedVersion) 178 | { 179 | return IncompatibleConfigurationHandlingOption.ERROR; 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /NeosModLoader/NeosModBase.cs: -------------------------------------------------------------------------------- 1 | namespace NeosModLoader 2 | { 3 | /// 4 | /// Contains public metadata about a mod. 5 | /// 6 | public abstract class NeosModBase 7 | { 8 | /// 9 | /// Gets the mod's name. This must be unique. 10 | /// 11 | public abstract string Name { get; } 12 | 13 | /// 14 | /// Gets the mod's author. 15 | /// 16 | public abstract string Author { get; } 17 | 18 | /// 19 | /// Gets the mod's semantic version. 20 | /// 21 | public abstract string Version { get; } 22 | 23 | /// 24 | /// Gets an optional hyperlink to the mod's homepage. 25 | /// 26 | public virtual string? Link { get; } 27 | 28 | /// 29 | /// A circular reference back to the LoadedNeosMod that contains this NeosModBase. 30 | /// The reference is set once the mod is successfully loaded, and is null before that. 31 | /// 32 | internal LoadedNeosMod? loadedNeosMod; 33 | 34 | /// 35 | /// Gets this mod's current . 36 | /// 37 | /// This will always be the same instance. 38 | /// 39 | /// This mod's current configuration. 40 | public ModConfiguration? GetConfiguration() 41 | { 42 | if (!FinishedLoading) 43 | { 44 | throw new ModConfigurationException($"GetConfiguration() was called before {Name} was done initializing. Consider calling GetConfiguration() from within OnEngineInit()"); 45 | } 46 | return loadedNeosMod?.ModConfiguration; 47 | } 48 | 49 | internal bool FinishedLoading { get; set; } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /NeosModLoader/NeosModLoader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {D4627C7F-8091-477A-ABDC-F1465D94D8D9} 5 | Library 6 | Properties 7 | NeosModLoader 8 | false 9 | NeosModLoader 10 | NeosModLoader 11 | NeosModLoaderHeadless 12 | $(AssemblyTitle).dll 13 | false 14 | net462 15 | 512 16 | 9.0 17 | enable 18 | true 19 | true 20 | false 21 | None 22 | True 23 | 24 | 25 | 26 | $(MSBuildThisFileDirectory)NeosVR/ 27 | C:\Program Files (x86)\Steam\steamapps\common\NeosVR\ 28 | $(HOME)/.steam/steam/steamapps/common/NeosVR/ 29 | D:/Files/Games/Neos/app/ 30 | 31 | 32 | 33 | none 34 | 35 | 36 | 37 | embedded 38 | 39 | 40 | 41 | 42 | 43 | 44 | $(NeosPath)Neos_Data\Managed\Newtonsoft.Json.dll 45 | 46 | 47 | $(NeosPath)Neos_Data\Managed\BaseX.dll 48 | 49 | 50 | $(NeosPath)Neos_Data\Managed\FrooxEngine.dll 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /NeosModLoader/NeosVersionReset.cs: -------------------------------------------------------------------------------- 1 | using BaseX; 2 | using FrooxEngine; 3 | using HarmonyLib; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Reflection.Emit; 11 | using System.Runtime.CompilerServices; 12 | using System.Security.Cryptography; 13 | 14 | namespace NeosModLoader 15 | { 16 | internal class NeosVersionReset 17 | { 18 | // used when AdvertiseVersion == true 19 | private const string NEOS_MOD_LOADER = "NeosModLoader.dll"; 20 | 21 | internal static void Initialize() 22 | { 23 | ModLoaderConfiguration config = ModLoaderConfiguration.Get(); 24 | Engine engine = Engine.Current; 25 | 26 | // get the version string before we mess with it 27 | string originalVersionString = engine.VersionString; 28 | 29 | List extraAssemblies = Engine.ExtraAssemblies; 30 | string assemblyFilename = Path.GetFileName(Assembly.GetExecutingAssembly().Location); 31 | bool nmlPresent = extraAssemblies.Remove(assemblyFilename); 32 | 33 | if (!nmlPresent) 34 | { 35 | throw new Exception($"Assertion failed: Engine.ExtraAssemblies did not contain \"{assemblyFilename}\""); 36 | } 37 | 38 | // get all PostX'd assemblies. This is useful, as plugins can't NOT be PostX'd. 39 | Assembly[] postxedAssemblies = AppDomain.CurrentDomain.GetAssemblies() 40 | .Where(IsPostXProcessed) 41 | .ToArray(); 42 | 43 | Logger.DebugFuncInternal(() => 44 | { 45 | string potentialPlugins = postxedAssemblies 46 | .Select(a => Path.GetFileName(a.Location)) 47 | .Join(delimiter: ", "); 48 | return $"Found {postxedAssemblies.Length} potential plugins: {potentialPlugins}"; 49 | }); 50 | 51 | HashSet expectedPostXAssemblies = GetExpectedPostXAssemblies(); 52 | 53 | // attempt to map the PostX'd assemblies to Neos's plugin list 54 | Dictionary plugins = new Dictionary(postxedAssemblies.Length); 55 | Assembly[] unmatchedAssemblies = postxedAssemblies 56 | .Where(assembly => 57 | { 58 | string filename = Path.GetFileName(assembly.Location); 59 | if (extraAssemblies.Contains(filename)) 60 | { 61 | // okay, the assembly's filename is in the plugin list. It's probably a plugin. 62 | plugins.Add(filename, assembly); 63 | return false; 64 | } 65 | else 66 | { 67 | // remove certain expected assemblies from the "unmatchedAssemblies" naughty list 68 | return !expectedPostXAssemblies.Contains(assembly); 69 | } 70 | }) 71 | .ToArray(); 72 | 73 | 74 | Logger.DebugFuncInternal(() => 75 | { 76 | string actualPlugins = plugins.Keys.Join(delimiter: ", "); 77 | return $"Found {plugins.Count} actual plugins: {actualPlugins}"; 78 | }); 79 | 80 | // warn about the assemblies we couldn't map to plugins 81 | foreach (Assembly assembly in unmatchedAssemblies) 82 | { 83 | Logger.WarnInternal($"Unexpected PostX'd assembly: \"{assembly.Location}\". If this is a plugin, then my plugin-detection code is faulty."); 84 | } 85 | 86 | // warn about the plugins we couldn't map to assemblies 87 | HashSet unmatchedPlugins = new(extraAssemblies); 88 | unmatchedPlugins.ExceptWith(plugins.Keys); // remove all matched plugins 89 | foreach (string plugin in unmatchedPlugins) 90 | { 91 | Logger.ErrorInternal($"Unmatched plugin: \"{plugin}\". NML could not find the assembly for this plugin, therefore NML cannot properly calculate the compatibility hash."); 92 | } 93 | 94 | // flags used later to determine how to spoof 95 | bool includePluginsInHash = true; 96 | 97 | // if unsafe is true, we should pretend there are no plugins and spoof everything 98 | if (config.Unsafe) 99 | { 100 | if (!config.AdvertiseVersion) 101 | { 102 | extraAssemblies.Clear(); 103 | } 104 | includePluginsInHash = false; 105 | Logger.WarnInternal("Unsafe mode is enabled! Not that you had a warranty, but now it's DOUBLE void!"); 106 | } 107 | // else if unmatched plugins are present, we should not spoof anything 108 | else if (unmatchedPlugins.Count != 0) 109 | { 110 | Logger.ErrorInternal("Version spoofing was not performed due to some plugins having missing assemblies."); 111 | return; 112 | } 113 | // else we should spoof normally 114 | 115 | 116 | // get plugin assemblies sorted in the same order Neos sorted them. 117 | List sortedPlugins = extraAssemblies 118 | .Select(path => plugins[path]) 119 | .ToList(); 120 | 121 | if (config.AdvertiseVersion) 122 | { 123 | // put NML back in the version string 124 | Logger.MsgInternal($"Adding {NEOS_MOD_LOADER} to version string because you have AdvertiseVersion set to true."); 125 | extraAssemblies.Insert(0, NEOS_MOD_LOADER); 126 | } 127 | 128 | // we intentionally attempt to set the version string first, so if it fails the compatibilty hash is left on the original value 129 | // this is to prevent the case where a player simply doesn't know their version string is wrong 130 | if (!SpoofVersionString(engine, originalVersionString)) 131 | { 132 | Logger.WarnInternal("Version string spoofing failed"); 133 | return; 134 | } 135 | 136 | if (!SpoofCompatibilityHash(engine, sortedPlugins, includePluginsInHash)) 137 | { 138 | Logger.WarnInternal("Compatibility hash spoofing failed"); 139 | return; 140 | } 141 | 142 | Logger.MsgInternal("Compatibility hash spoofing succeeded"); 143 | } 144 | 145 | private static bool IsPostXProcessed(Assembly assembly) 146 | { 147 | return assembly.Modules // in practice there will only be one module, and it will have the dll's name 148 | .SelectMany(module => module.GetCustomAttributes()) 149 | .Where(IsPostXProcessedAttribute) 150 | .Any(); 151 | } 152 | 153 | private static bool IsPostXProcessedAttribute(DescriptionAttribute descriptionAttribute) 154 | { 155 | return descriptionAttribute.Description == "POSTX_PROCESSED"; 156 | } 157 | 158 | // get all the non-plugin PostX'd assemblies we expect to exist 159 | private static HashSet GetExpectedPostXAssemblies() 160 | { 161 | List list = new() 162 | { 163 | Type.GetType("FrooxEngine.IComponent, FrooxEngine")?.Assembly, 164 | Type.GetType("BusinessX.NeosClassroom, BusinessX")?.Assembly, 165 | Assembly.GetExecutingAssembly(), 166 | }; 167 | return list 168 | .Where(assembly => assembly != null) 169 | .ToHashSet()!; 170 | } 171 | 172 | private static bool SpoofCompatibilityHash(Engine engine, List plugins, bool includePluginsInHash) 173 | { 174 | string vanillaCompatibilityHash; 175 | int? vanillaProtocolVersionMaybe = GetVanillaProtocolVersion(); 176 | if (vanillaProtocolVersionMaybe is int vanillaProtocolVersion) 177 | { 178 | Logger.DebugFuncInternal(() => $"Vanilla protocol version is {vanillaProtocolVersion}"); 179 | vanillaCompatibilityHash = CalculateCompatibilityHash(vanillaProtocolVersion, plugins, includePluginsInHash); 180 | return SetCompatibilityHash(engine, vanillaCompatibilityHash); 181 | } 182 | else 183 | { 184 | Logger.ErrorInternal("Unable to determine vanilla protocol version"); 185 | return false; 186 | } 187 | } 188 | 189 | private static string CalculateCompatibilityHash(int ProtocolVersion, List plugins, bool includePluginsInHash) 190 | { 191 | using MD5CryptoServiceProvider cryptoServiceProvider = new(); 192 | ConcatenatedStream inputStream = new(); 193 | inputStream.EnqueueStream(new MemoryStream(BitConverter.GetBytes(ProtocolVersion))); 194 | if (includePluginsInHash) 195 | { 196 | foreach (Assembly plugin in plugins) 197 | { 198 | FileStream fileStream = File.OpenRead(plugin.Location); 199 | fileStream.Seek(375L, SeekOrigin.Current); 200 | inputStream.EnqueueStream(fileStream); 201 | } 202 | } 203 | byte[] hash = cryptoServiceProvider.ComputeHash(inputStream); 204 | return Convert.ToBase64String(hash); 205 | } 206 | 207 | private static bool SetCompatibilityHash(Engine engine, string Target) 208 | { 209 | // This is super sketchy and liable to break with new compiler versions. 210 | // I have a good reason for doing it though... if I just called the setter it would recursively 211 | // end up calling itself, because I'm HOOKINGthe CompatibilityHash setter. 212 | FieldInfo field = AccessTools.DeclaredField(typeof(Engine), $"<{nameof(Engine.CompatibilityHash)}>k__BackingField"); 213 | 214 | if (field == null) 215 | { 216 | Logger.WarnInternal("Unable to write Engine.CompatibilityHash"); 217 | return false; 218 | } 219 | else 220 | { 221 | Logger.DebugFuncInternal(() => $"Changing compatibility hash from {engine.CompatibilityHash} to {Target}"); 222 | field.SetValue(engine, Target); 223 | return true; 224 | } 225 | } 226 | 227 | private static bool SpoofVersionString(Engine engine, string originalVersionString) 228 | { 229 | FieldInfo field = AccessTools.DeclaredField(engine.GetType(), "_versionString"); 230 | if (field == null) 231 | { 232 | Logger.WarnInternal("Unable to write Engine._versionString"); 233 | return false; 234 | } 235 | // null the cached value 236 | field.SetValue(engine, null); 237 | 238 | Logger.DebugFuncInternal(() => $"Changing version string from {originalVersionString} to {engine.VersionString}"); 239 | return true; 240 | } 241 | 242 | // perform incredible bullshit to rip the hardcoded protocol version out of the dang IL 243 | private static int? GetVanillaProtocolVersion() 244 | { 245 | // raw IL immediately surrounding the number we need to find, which in this example is 770 246 | // ldc.i4 770 247 | // call unsigned int8[] [mscorlib]System.BitConverter::GetBytes(int32) 248 | 249 | // we're going to search for that method call, then grab the operand of the ldc.i4 that precedes it 250 | 251 | MethodInfo targetCallee = AccessTools.DeclaredMethod(typeof(BitConverter), nameof(BitConverter.GetBytes), new Type[] { typeof(int) }); 252 | if (targetCallee == null) 253 | { 254 | Logger.ErrorInternal("Could not find System.BitConverter::GetBytes(System.Int32)"); 255 | return null; 256 | } 257 | 258 | MethodInfo initializeShim = AccessTools.DeclaredMethod(typeof(Engine), nameof(Engine.Initialize)); 259 | if (initializeShim == null) 260 | { 261 | Logger.ErrorInternal("Could not find Engine.Initialize(*)"); 262 | return null; 263 | } 264 | 265 | AsyncStateMachineAttribute asyncAttribute = (AsyncStateMachineAttribute)initializeShim.GetCustomAttribute(typeof(AsyncStateMachineAttribute)); 266 | if (asyncAttribute == null) 267 | { 268 | Logger.ErrorInternal("Could not find AsyncStateMachine for Engine.Initialize"); 269 | return null; 270 | } 271 | 272 | // async methods are weird. Their body is just some setup code that passes execution... elsewhere. 273 | // The compiler generates a companion type for async methods. This companion type has some ridiculous nondeterministic name, but luckily 274 | // we can just ask this attribute what the type is. The companion type should have a MoveNext() method that contains the actual IL we need. 275 | Type asyncStateMachineType = asyncAttribute.StateMachineType; 276 | MethodInfo initializeImpl = AccessTools.DeclaredMethod(asyncStateMachineType, "MoveNext"); 277 | if (initializeImpl == null) 278 | { 279 | Logger.ErrorInternal("Could not find MoveNext method for Engine.Initialize"); 280 | return null; 281 | } 282 | 283 | List instructions = PatchProcessor.GetOriginalInstructions(initializeImpl); 284 | for (int i = 1; i < instructions.Count; i++) 285 | { 286 | if (instructions[i].Calls(targetCallee)) 287 | { 288 | // we're guaranteed to have a previous instruction because we began iteration from 1 289 | CodeInstruction previous = instructions[i - 1]; 290 | if (OpCodes.Ldc_I4.Equals(previous.opcode)) 291 | { 292 | return (int)previous.operand; 293 | } 294 | } 295 | } 296 | 297 | return null; 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /NeosModLoader/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("NeosModLoader")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("NeosModLoader")] 13 | [assembly: AssemblyCopyright("Copyright © 2021")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("d4627c7f-8091-477a-abdc-f1465d94d8d9")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion(NeosModLoader.ModLoader.VERSION_CONSTANT)] 36 | [assembly: AssemblyFileVersion(NeosModLoader.ModLoader.VERSION_CONSTANT)] 37 | 38 | // prevent PostX from modifying my assembly, as it doesn't need anything done to it 39 | // this keeps PostX from overwriting my AssemblyVersionAttribute 40 | [module: Description("POSTX_PROCESSED")] 41 | -------------------------------------------------------------------------------- /NeosModLoader/SplashChanger.cs: -------------------------------------------------------------------------------- 1 | using FrooxEngine; 2 | using System; 3 | using System.Reflection; 4 | 5 | namespace NeosModLoader 6 | { 7 | // Custom splash screen logic failing shouldn't fail the rest of the modloader. 8 | // Keep that in mind when editing later on. 9 | internal static class SplashChanger 10 | { 11 | private static bool failed = false; 12 | 13 | private static MethodInfo? _updatePhase = null; 14 | private static MethodInfo? UpdatePhase 15 | { 16 | get 17 | { 18 | if (_updatePhase is null) 19 | { 20 | try 21 | { 22 | _updatePhase = typeof(Engine) 23 | .GetMethod("UpdateInitPhase", BindingFlags.NonPublic | BindingFlags.Instance); 24 | } 25 | catch (Exception ex) 26 | { 27 | if (!failed) 28 | { 29 | Logger.WarnInternal("UpdatePhase not found: " + ex.ToString()); 30 | } 31 | failed = true; 32 | } 33 | } 34 | return _updatePhase; 35 | } 36 | } 37 | private static MethodInfo? _updateSubPhase = null; 38 | private static MethodInfo? UpdateSubPhase 39 | { 40 | get 41 | { 42 | if (_updateSubPhase is null) 43 | { 44 | try 45 | { 46 | _updateSubPhase = typeof(Engine) 47 | .GetMethod("UpdateInitSubphase", BindingFlags.NonPublic | BindingFlags.Instance); 48 | } 49 | catch (Exception ex) 50 | { 51 | if (!failed) 52 | { 53 | Logger.WarnInternal("UpdateSubPhase not found: " + ex.ToString()); 54 | } 55 | failed = true; 56 | } 57 | } 58 | return _updateSubPhase; 59 | } 60 | } 61 | 62 | // Returned true means success, false means something went wrong. 63 | internal static bool SetCustom(string text) 64 | { 65 | if (ModLoaderConfiguration.Get().HideVisuals) return true; 66 | try 67 | { 68 | // VerboseInit does extra logging, so turning it if off while we change the phase. 69 | bool ogVerboseInit = Engine.Current.VerboseInit; 70 | Engine.Current.VerboseInit = false; 71 | UpdatePhase?.Invoke(Engine.Current, new object[] { "~ NeosModLoader ~", false }); 72 | UpdateSubPhase?.Invoke(Engine.Current, new object[] { text, false }); 73 | Engine.Current.VerboseInit = ogVerboseInit; 74 | return true; 75 | } 76 | catch (Exception ex) 77 | { 78 | if (!failed) 79 | { 80 | Logger.WarnInternal("Splash change failed: " + ex.ToString()); 81 | failed = true; 82 | } 83 | return false; 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /NeosModLoader/Util.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Security.Cryptography; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace NeosModLoader 12 | { 13 | internal static class Util 14 | { 15 | /// 16 | /// Get the executing mod by stack trace analysis. 17 | /// You may skip extra frames if you know your callers are guaranteed to be NML code. 18 | /// 19 | /// A stack trace captured by the callee 20 | /// The executing mod, or null if none found 21 | internal static NeosMod? ExecutingMod(StackTrace stackTrace) 22 | { 23 | for (int i = 0; i < stackTrace.FrameCount; i++) 24 | { 25 | Assembly? assembly = stackTrace.GetFrame(i)?.GetMethod()?.DeclaringType?.Assembly; 26 | if (assembly != null && ModLoader.AssemblyLookupMap.TryGetValue(assembly, out NeosMod mod)) 27 | { 28 | return mod; 29 | } 30 | } 31 | return null; 32 | } 33 | 34 | /// 35 | /// Used to debounce calls to a given method. The given method will be called after there have been no additional calls 36 | /// for the given number of milliseconds. 37 | /// 38 | /// The returned by this method has internal state used for debouncing, 39 | /// so you will need to store and reuse the Action for each call. 40 | /// 41 | /// The type of the debounced method's input. 42 | /// The method to be debounced. 43 | /// How long to wait before a call to the debounced method gets passed through. 44 | /// A debouncing wrapper for the given method. 45 | // credit: https://stackoverflow.com/questions/28472205/c-sharp-event-debounce 46 | internal static Action Debounce(this Action func, int milliseconds) 47 | { 48 | // this variable gets embedded in the returned Action via the magic of closures 49 | CancellationTokenSource? cancelTokenSource = null; 50 | 51 | return arg => 52 | { 53 | // if there's already a scheduled call, then cancel it 54 | cancelTokenSource?.Cancel(); 55 | cancelTokenSource = new CancellationTokenSource(); 56 | 57 | // schedule a new call 58 | Task.Delay(milliseconds, cancelTokenSource.Token) 59 | .ContinueWith(t => 60 | { 61 | if (t.IsCompletedSuccessfully()) 62 | { 63 | Task.Run(() => func(arg)); 64 | } 65 | }, TaskScheduler.Default); 66 | }; 67 | } 68 | 69 | // shim because this doesn't exist in .NET 4.6 70 | private static bool IsCompletedSuccessfully(this Task t) 71 | { 72 | return t.IsCompleted && !t.IsFaulted && !t.IsCanceled; 73 | } 74 | 75 | //credit to delta for this method https://github.com/XDelta/ 76 | internal static string GenerateSHA256(string filepath) 77 | { 78 | using var hasher = SHA256.Create(); 79 | using var stream = File.OpenRead(filepath); 80 | var hash = hasher.ComputeHash(stream); 81 | return BitConverter.ToString(hash).Replace("-", ""); 82 | } 83 | 84 | internal static HashSet ToHashSet(this IEnumerable source, IEqualityComparer? comparer = null) 85 | { 86 | return new HashSet(source, comparer); 87 | } 88 | 89 | // check if a type cannot possibly have null assigned 90 | internal static bool CannotBeNull(Type t) 91 | { 92 | return t.IsValueType && Nullable.GetUnderlyingType(t) == null; 93 | } 94 | 95 | // check if a type is allowed to have null assigned 96 | internal static bool CanBeNull(Type t) 97 | { 98 | return !CannotBeNull(t); 99 | } 100 | 101 | internal static IEnumerable GetLoadableTypes(this Assembly assembly, Predicate predicate) 102 | { 103 | try 104 | { 105 | return assembly.GetTypes().Where(type => CheckType(type, predicate)); 106 | } 107 | catch (ReflectionTypeLoadException e) 108 | { 109 | return e.Types.Where(type => CheckType(type, predicate)); 110 | } 111 | } 112 | 113 | // check a potentially unloadable type to see if it is (A) loadable and (B) satsifies a predicate without throwing an exception 114 | // this does a series of increasingly aggressive checks to see if the type is unsafe to touch 115 | private static bool CheckType(Type type, Predicate predicate) 116 | { 117 | if (type == null) 118 | { 119 | return false; 120 | } 121 | 122 | try 123 | { 124 | string _name = type.Name; 125 | } 126 | catch (Exception e) 127 | { 128 | Logger.DebugFuncInternal(() => $"Could not read the name for a type: {e}"); 129 | return false; 130 | } 131 | 132 | try 133 | { 134 | return predicate(type); 135 | } 136 | catch (Exception e) 137 | { 138 | Logger.DebugFuncInternal(() => $"Could not load type \"{type}\": {e}"); 139 | return false; 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /NeosModLoader/Utility/EnumerableInjector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace NeosModLoader.Utility 6 | { 7 | /// 8 | /// Provides the ability to inject actions into the execution of an enumeration while transforming it.

9 | /// This example shows how to apply the when patching a function.
10 | /// Of course you typically wouldn't patch with a generic method, that's just for illustrating the Type usage. 11 | /// 12 | /// private static void Postfix<Original, Transformed>(ref IEnumerable<Original> __result) where Transformed : Original 13 | /// { 14 | /// __result = new EnumerableInjector<Original, Transformed>(__result, 15 | /// item => { Msg("Change what the item is exactly"); return new Transformed(item); }) 16 | /// { 17 | /// Prefix = () => Msg("Before the first item is returned"), 18 | /// PreItem = item => { Msg("Decide if an item gets returned"); return true; }, 19 | /// PostItem = (original, transformed, returned) => Msg("After control would come back to the generator after a yield return"), 20 | /// Postfix = () => Msg("When the generator stopped returning items") 21 | /// }; 22 | /// } 23 | /// 24 | ///
25 | /// The type of the original enumeration's items. 26 | /// The type of the transformed enumeration's items.
Must be assignable to TOriginal for compatibility.
27 | public class EnumerableInjector : IEnumerable 28 | where TTransformed : TOriginal 29 | { 30 | /// 31 | /// Internal enumerator for iteration. 32 | /// 33 | private readonly IEnumerator enumerator; 34 | 35 | private Action postfix = () => { }; 36 | private Action postItem = (original, transformed, returned) => { }; 37 | private Action prefix = () => { }; 38 | private Func preItem = item => true; 39 | private Func transformItem = item => throw new NotImplementedException("You're supposed to insert your own transformation function here!"); 40 | 41 | /// 42 | /// Gets called when the wrapped enumeration returned the last item. 43 | /// 44 | public Action Postfix 45 | { 46 | get => postfix; 47 | set => postfix = value ?? throw new ArgumentNullException(nameof(value), "Postfix can't be null!"); 48 | } 49 | 50 | /// 51 | /// Gets called for each item, with the transformed item, and whether it was passed through. 52 | /// First thing to be called after execution returns to the enumerator after a yield return. 53 | /// 54 | public Action PostItem 55 | { 56 | get => postItem; 57 | set => postItem = value ?? throw new ArgumentNullException(nameof(value), "PostItem can't be null!"); 58 | } 59 | 60 | /// 61 | /// Gets called before the enumeration returns the first item. 62 | /// 63 | public Action Prefix 64 | { 65 | get => prefix; 66 | set => prefix = value ?? throw new ArgumentNullException(nameof(value), "Prefix can't be null!"); 67 | } 68 | 69 | /// 70 | /// Gets called for each item to determine whether it should be passed through. 71 | /// 72 | public Func PreItem 73 | { 74 | get => preItem; 75 | set => preItem = value ?? throw new ArgumentNullException(nameof(value), "PreItem can't be null!"); 76 | } 77 | 78 | /// 79 | /// Gets called for each item to transform it, even if it won't be passed through. 80 | /// 81 | public Func TransformItem 82 | { 83 | get => transformItem; 84 | set => transformItem = value ?? throw new ArgumentNullException(nameof(value), "TransformItem can't be null!"); 85 | } 86 | 87 | /// 88 | /// Creates a new instance of the class using the supplied input and transform function. 89 | /// 90 | /// The enumerable to inject into and transform. 91 | /// The transformation function. 92 | public EnumerableInjector(IEnumerable enumerable, Func transformItem) 93 | : this(enumerable.GetEnumerator(), transformItem) 94 | { } 95 | 96 | /// 97 | /// Creates a new instance of the class using the supplied input and transform function. 98 | /// 99 | /// The enumerator to inject into and transform. 100 | /// The transformation function. 101 | public EnumerableInjector(IEnumerator enumerator, Func transformItem) 102 | { 103 | this.enumerator = enumerator; 104 | TransformItem = transformItem; 105 | } 106 | 107 | /// 108 | /// Injects into and transforms the input enumeration. 109 | /// 110 | /// The injected and transformed enumeration. 111 | public IEnumerator GetEnumerator() 112 | { 113 | Prefix(); 114 | 115 | while (enumerator.MoveNext()) 116 | { 117 | var item = enumerator.Current; 118 | var returnItem = PreItem(item); 119 | var transformedItem = TransformItem(item); 120 | 121 | if (returnItem) 122 | yield return transformedItem; 123 | 124 | PostItem(item, transformedItem, returnItem); 125 | } 126 | 127 | Postfix(); 128 | } 129 | 130 | /// 131 | /// Injects into and transforms the input enumeration without a generic type. 132 | /// 133 | /// The injected and transformed enumeration without a generic type. 134 | IEnumerator IEnumerable.GetEnumerator() 135 | { 136 | return GetEnumerator(); 137 | } 138 | } 139 | 140 | /// 141 | /// Provides the ability to inject actions into the execution of an enumeration without transforming it.

142 | /// This example shows how to apply the when patching a function.
143 | /// Of course you typically wouldn't patch with a generic method, that's just for illustrating the Type usage. 144 | /// 145 | /// static void Postfix<T>(ref IEnumerable<T> __result) 146 | /// { 147 | /// __result = new EnumerableInjector<T>(__result) 148 | /// { 149 | /// Prefix = () => Msg("Before the first item is returned"), 150 | /// PreItem = item => { Msg("Decide if an item gets returned"); return true; }, 151 | /// TransformItem = item => { Msg("Change what the item is exactly"); return item; }, 152 | /// PostItem = (original, transformed, returned) => Msg("After control would come back to the generator after a yield return"), 153 | /// Postfix = () => Msg("When the generator stopped returning items") 154 | /// }; 155 | /// } 156 | /// 157 | ///
158 | /// The type of the enumeration's items. 159 | public class EnumerableInjector : EnumerableInjector 160 | { 161 | /// 162 | /// Creates a new instance of the class using the supplied input . 163 | /// 164 | /// The enumerable to inject into. 165 | public EnumerableInjector(IEnumerable enumerable) 166 | : this(enumerable.GetEnumerator()) 167 | { } 168 | 169 | /// 170 | /// Creates a new instance of the class using the supplied input . 171 | /// 172 | /// The enumerator to inject into. 173 | public EnumerableInjector(IEnumerator enumerator) 174 | : base(enumerator, item => item) 175 | { } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /NeosModLoader/Utility/PlatformHelper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace NeosModLoader.Utility 4 | { 5 | // Provides helper functions for platform-specific operations. 6 | // Used for cases such as file handling which can vary between platforms. 7 | internal class PlatformHelper 8 | { 9 | public static readonly string AndroidNeosPath = "/sdcard/ModData/com.Solirax.Neos"; 10 | 11 | // Android does not support Directory.GetCurrentDirectory(), so will fall back to the root '/' directory. 12 | public static bool UseFallbackPath() => Directory.GetCurrentDirectory().Replace('\\', '/') == "/" && !Directory.Exists("/Neos_Data"); 13 | public static bool IsPathEmbedded(string path) => path.StartsWith("/data/app/com.Solirax.Neos"); 14 | 15 | public static string MainDirectory 16 | { 17 | get { return UseFallbackPath() ? AndroidNeosPath : Directory.GetCurrentDirectory(); } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeosModLoader 2 | 3 | A mod loader for [Neos VR](https://neos.com/). Consider joining our community on [Discord][Neos Modding Discord] for support, updates, and more. 4 | 5 | ## Installation 6 | 7 | If you are using the Steam version of Neos you are in the right place. If you are using the standalone version, read the [Neos Standalone Setup](doc/neos_standalone_setup.md) instructions. If you are on Linux, read the [Linux Notes](doc/linux.md). 8 | 9 | 1. Download [NeosModLoader.dll](https://github.com/neos-modding-group/NeosModLoader/releases/latest/download/NeosModLoader.dll) to Neos's `Libraries` folder (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Libraries`). 10 | 2. Place [0Harmony.dll](https://github.com/neos-modding-group/NeosModLoader/releases/latest/download/0Harmony.dll) into a `nml_libs` folder under your Neos install directory (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR\nml_libs`). You will need to create this folder. 11 | 3. Add mod DLL files to a `nml_mods` folder under your Neos install directory (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR\nml_mods`). You can create the folder if it's missing, or simply launch Neos once with NeosModLoader installed and it will be created automatically. 12 | 4. Add the following to Neos's [launch options](https://wiki.neos.com/Command_Line_Arguments): `-LoadAssembly Libraries\NeosModLoader.dll`, substituting the path for wherever you put `NeosModLoader.dll`. 13 | 5. Start the game. If you want to verify that NeosModLoader is working you can check the Neos logs. (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Logs`). The modloader adds some very obvious logs on startup, and if they're missing something has gone wrong. Here is an [example log file](doc/example_log.log) where everything worked correctly. 14 | 15 | If NeosModLoader isn't working after following those steps, take a look at our [troubleshooting page](doc/troubleshooting.md). 16 | 17 | ### Example Directory Structure 18 | 19 | Your Neos directory should now look similar to the following. Files not related to modding are not shown. 20 | 21 | ``` 22 | 23 | │ Neos.exe 24 | │ NeosLauncher.exe 25 | │ 26 | ├───Logs 27 | │ 28 | │ 29 | ├───nml_mods 30 | │ InspectorScroll.dll 31 | │ MotionBlurDisable.dll 32 | │ NeosContactsSort.dll 33 | | 34 | ├───nml_libs 35 | │ 0Harmony.dll 36 | | 37 | │ 38 | └───Libraries 39 | NeosModLoader.dll 40 | ``` 41 | 42 | Note that the libraries can also be in the root of the Neos install directory if you prefer, but the loading of those happens outside of NML itself. 43 | 44 | ## Finding Mods 45 | 46 | A list of known mods is available in the [Neos Mod List](https://www.neosmodloader.com/mods). New mods and updates are also announced in [our Discord][Neos Modding Discord]. 47 | 48 | ## Frequently Asked Questions 49 | 50 | Many questions about what NML is and how it works are answered on our [frequently asked questions page](doc/faq.md). 51 | 52 | ## Making a Mod 53 | 54 | Check out the [Mod Creation Guide](doc/making_mods.md). 55 | 56 | ## Configuration 57 | 58 | NeosModLoader aims to have a reasonable default configuration, but certain things can be adjusted via an [optional config file](doc/modloader_config.md). 59 | 60 | ## Contributing 61 | 62 | Issues and PRs are welcome. Please read our [Contributing Guidelines](.github/CONTRIBUTING.md)! 63 | 64 | ## Licensing and Credits 65 | 66 | NeosModLoader is licensed under the GNU Lesser General Public License (LGPL). See [LICENSE.txt](LICENSE.txt) for the full license. 67 | 68 | Third-party libraries distributed alongside NeosModLoader: 69 | 70 | - [LibHarmony] ([MIT License](https://github.com/pardeike/Harmony/blob/v2.2.1.0/LICENSE)) 71 | 72 | Third-party libraries used in source: 73 | 74 | - [.NET](https://github.com/dotnet) (Various licenses) 75 | - [Neos VR](https://neos.com/) ([EULA](https://store.steampowered.com/eula/740250_eula_0)) 76 | - [Json.NET](https://github.com/JamesNK/Newtonsoft.Json) ([MIT License](https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md)) 77 | 78 | 79 | [LibHarmony]: https://github.com/pardeike/Harmony 80 | [Neos Modding Discord]: https://discord.gg/vCDJK9xyvm 81 | -------------------------------------------------------------------------------- /doc/config.md: -------------------------------------------------------------------------------- 1 | # NeosModLoader Configuration System 2 | 3 | NeosModLoader provides a built-in configuration system that can be used to persist configuration values for mods. **This configuration system only exists in NeosModLoader releases 1.8.0 and later!** 4 | 5 | Operations provided: 6 | 7 | - Reading value of a config key 8 | - Writing value to a config key 9 | - Enumerating config keys for a mod 10 | - Enumerating mods 11 | - Saving a config to disk 12 | 13 | Behind the scenes, configs are saved to a `nml_config` folder in the Neos install directory. The `nml_config` folder contains JSON files, named after each mod dll that defines a config. End users and mod developers do not need to interact with this JSON directly. Mod developers should use the API exposed by NeosModLoader. End users should use interfaces exposed by configuration management mods. 14 | 15 | ## Overview 16 | 17 | - Mods may define a configuration 18 | - Configuration items must be declared alongside the mod itself. You cannot change your configuration schema at runtime. 19 | - Configuration items may be of any type, however, there are considerations: 20 | - Json.NET is used to serialize the configuration, so the type must be JSON-compatible (e.g. no circular references). Lists, Sets, and Dictionary will work fine. 21 | - Using complex types will make it more difficult for configuration manager UIs to interface with your mod. For best compatibility keep things simple (primitive types and basic collections) 22 | - Reading/writing configuration values is done in-memory and is extremely cheap. 23 | - Saving configuration to disk is more expensive but is done infrequently 24 | 25 | ## Working With Your Mod's Configuration 26 | 27 | A simple example is below: 28 | 29 | ```csharp 30 | public class NeosModConfigurationExample : NeosMod 31 | { 32 | public override string Name => "NeosModConfigurationExample"; 33 | public override string Author => "runtime"; 34 | public override string Version => "1.0.0"; 35 | public override string Link => "https://github.com/neos-modding-group/NeosModConfigurationExample"; 36 | 37 | [AutoRegisterConfigKey] 38 | private readonly ModConfigurationKey KEY_COUNT = new ModConfigurationKey("count", "Example counter", internalAccessOnly: true); 39 | 40 | public override void OnEngineInit() 41 | { 42 | ModConfiguration config = GetConfiguration(); 43 | int countValue = default(int); 44 | if (config.TryGetValue(KEY_COUNT, out countValue)) 45 | { 46 | int oldValue = countValue++; 47 | Msg($"Incrementing count from {oldValue} to {countValue}"); 48 | } 49 | else 50 | { 51 | Msg($"Initializing count to {countValue}"); 52 | } 53 | 54 | config.Set(KEY_COUNT, countValue); 55 | } 56 | } 57 | ``` 58 | 59 | A full example repository that uses a few additional APIs is provided [here](https://github.com/neos-modding-group/NeosModConfigurationExample). 60 | 61 | ### Defining a Configuration 62 | 63 | To define a configuration simply have at least one `ModConfigurationKey` field with the `[AutoRegisterConfigKey]` attribute applied. 64 | 65 | If you need more options, implement the optional `DefineConfiguration` method in your mod. Here's an example: 66 | 67 | ```csharp 68 | // this override lets us change optional settings in our configuration definition 69 | public override void DefineConfiguration(ModConfigurationDefinitionBuilder builder) 70 | { 71 | builder 72 | .Version(new Version(1, 0, 0)) // manually set config version (default is 1.0.0) 73 | .AutoSave(false); // don't autosave on Neos shutdown (default is true) 74 | } 75 | ``` 76 | 77 | This `ModConfigurationDefinitionBuilder` allows you to change the default version and autosave values. [Version](#configuration-version) and [AutoSave](#saving-the-configuration) will be discussed in separate sections.. 78 | 79 | #### Configuration Version 80 | 81 | You may optionally specify a version for your configuration. This is separate from your mod's version. By default, the version will be 1.0.0. The version should be a [semantic version][semver]—in summary the major version should be bumped for hard breaking changes, and the minor version should be bumped if you break backwards compatibility. NeosModLoader uses this version number to check the saved configuration against your definition and ensure they are compatible. 82 | 83 | #### Configuration Keys 84 | 85 | Configuration keys define the values your mod's config can store. The relevant class is `ModConfigurationKey`, which has the following constructor: 86 | 87 | ```csharp 88 | public ModConfigurationKey(string name, string description, Func computeDefault = null, bool internalAccessOnly = false, Predicate valueValidator = null) 89 | ``` 90 | 91 | |Parameter | Description | Default | 92 | | -------- | ----------- | ------- | 93 | | name | Unique name of this config item | *required* | 94 | | description | Human-readable description of this config item | *required* | 95 | | computeDefault | Function that, if present, computes a default value for this key | `null` | 96 | | internalAccessOnly | If true, only the owning mod should have access to this config item. Note that this is *not* enforced by NeosModLoader itself. | `false` | 97 | | valueValidator | A custom function that (if present) checks if a value is valid for this configuration item | `null` | 98 | 99 | ### Saving the Configuration 100 | 101 | Configurations should be saved to disk by calling the `ModConfiguration.Save()` method. If you don't call `ModConfiguration.Save()`, your changes will still be available in memory. This allows multiple changes to be batched before you write them all to disk at once. Saving to disk is a relatively expensive operation and should not be performed at high frequency. 102 | 103 | NeosModLoader will automatically call `Save()` for you when Neos is shutting down. This will not occur if Neos crashes, so to avoid data loss you should manually call `Save()` when appropriate. If you'd like to opt out of this autosave-on-shutdown functionality, use the `ModConfigurationDefinitionBuilder` discussed in the [Defining a Configuration](#defining-a-configuration) section. 104 | 105 | ### Getting the Configuration 106 | 107 | To get the configuration, call `NeosModBase.GetConfiguration()`. Some notes: 108 | 109 | - This will return `null` if the mod does not have a configuration. 110 | - You must not call `NeosModBase.GetConfiguration()` before OnEngineInit() is called, as the mod may still be initializing. 111 | - The returned `ModConfiguration` instance is guaranteed to be the same reference for all calls to `NeosModBase.GetConfiguration()`. Therefore, it is safe to save a reference to your `ModConfiguration`. 112 | - Other mods may modify the `ModConfiguration` instance you are working with. 113 | - A `ModConfiguration.TryGetValue()` call will always return the current value for that config item. If you need notice that someone else has changed one of your configs, there are events you can subscribe to. However, the `ModConfiguration.GetValue()` and `TryGetValue()` API is very inexpensive so it is fine to poll. 114 | 115 | ### Events 116 | 117 | The `ModConfiguration` class provides two events you can subscribe to: 118 | 119 | - The static event `OnAnyConfigurationChanged` is called if any config value for any mod changed. 120 | - The instance event `OnThisConfigurationChanged` is called if one of the values in this mod's config changed. 121 | 122 | Both of these events use the following delegate: 123 | 124 | ```csharp 125 | public delegate void ConfigurationChangedEventHandler(ConfigurationChangedEvent configurationChangedEvent); 126 | ``` 127 | 128 | A `ConfigurationChangedEvent` has the following properties: 129 | 130 | - `ModConfiguration Config` is the configuration the change occurred in 131 | - `ModConfigurationKey Key` is the specific key who's value changed 132 | - `string Label` is a custom label that may be set by whoever changed the configuration. This may be `null`. 133 | 134 | ### Handling Incompatible Configuration Versions 135 | 136 | You may optionally override a `HandleIncompatibleConfigurationVersions()` function in your NeosMod to define how incompatible versions are handled. You have two options: 137 | 138 | - `IncompatibleConfigurationHandlingOption.ERROR`: Fail to read the config, and block saving over the config on disk. 139 | - `IncompatibleConfigurationHandlingOption.CLOBBER`: Destroy the saved config and start over from scratch. 140 | - `IncompatibleConfigurationHandlingOption.FORCE_LOAD`: Ignore the version number and load the config anyways. This may throw exceptions and break your mod. 141 | 142 | If you do not override `HandleIncompatibleConfigurationVersions()`, the default is to return `ERROR` on all incompatibilities. `HandleIncompatibleConfigurationVersions()` is only called for configs that are detected to be incompatible under [semantic versioning][semver]. 143 | 144 | Here's an example implementation that can detect mod downgrades and conditionally avoid clobbering your new config: 145 | 146 | ```csharp 147 | public override IncompatibleConfigurationHandlingOption HandleIncompatibleConfigurationVersions(Version serializedVersion, Version definedVersion) 148 | { 149 | if (serializedVersion > definedVersion) 150 | { 151 | // someone has dared to downgrade my mod 152 | // this will break the old version instead of nuking my config 153 | return IncompatibleConfigurationHandlingOption.ERROR; 154 | } 155 | else 156 | { 157 | // there's an old incompatible config version on disk 158 | // lets just nuke it instead of breaking 159 | return IncompatibleConfigurationHandlingOption.CLOBBER; 160 | } 161 | } 162 | ``` 163 | 164 | ### Breaking Changes in Configuration Definition 165 | 166 | There are two cases to consider: 167 | 168 | - **Forwards Compatible**: Can mod v2 load config v1? 169 | - **Backwards Compatible**: Can mod v1 load config v2? 170 | 171 | | Action | Forwards Compatible | Backwards Compatible | 172 | | ------ | ------------------- | ---------------------| 173 | | Adding a brand-new key | Yes | Yes | 174 | | Removing an existing key | Yes | Yes | 175 | | Adding, altering, or removing a key's default value | Yes | Maybe* | 176 | | Restricting a key's validator | Yes** | Yes | 177 | | Relaxing a key's validator | Yes | Maybe* | 178 | | Changing `internalAccessOnly` to `false` | Yes | Maybe* | 179 | | Changing `internalAccessOnly` to `true` | Yes** |Yes | 180 | | Altering a key's type (removing and re-adding later counts!) | **No** | **No** | 181 | 182 | \* NeosModLoader is compatible, but the old version of your mod's code may not be 183 | \*\* Assuming the new version of your mod properly accounts for reading old configs 184 | 185 | ## Working With Other Mods' Configurations 186 | 187 | An example of enumerating all configs: 188 | 189 | ```csharp 190 | void EnumerateConfigs() 191 | { 192 | IEnumerable mods = ModLoader.Mods(); 193 | foreach (NeosModBase mod in mods) 194 | { 195 | ModConfiguration config = mod.GetConfiguration(); 196 | if (config != null) 197 | { 198 | foreach (ModConfigurationKey key in config.ConfigurationItemDefinitions) 199 | { 200 | if (!key.InternalAccessOnly) // while we COULD read internal configs, we shouldn't. 201 | { 202 | if (config.TryGetValue(key, out object value)) 203 | { 204 | Msg($"{mod.Name} has configuration {key.Name} with type {key.ValueType()} and value {value}"); 205 | } 206 | else 207 | { 208 | Msg($"{mod.Name} has configuration {key.Name} with type {key.ValueType()} and no value"); 209 | } 210 | } 211 | } 212 | } 213 | } 214 | } 215 | ``` 216 | 217 | Worth noting here is that this API works with raw untyped objects, because as an external mod you lack the compile-time type information. The API performs its own type checking behind the scenes to prevent incorrect types from being written. 218 | 219 | [semver]: https://semver.org/ 220 | -------------------------------------------------------------------------------- /doc/directories.md: -------------------------------------------------------------------------------- 1 | # Neos Directories 2 | 3 | If you've installed to a non-default location then finding the path is up to you. 4 | 5 | | Directory | Description | 6 | | --------- |------------ | 7 | | Neos Install Directory | Contains the game install itself, the log directory, and the `Libraries` directory | 8 | | Log Directory | A `Logs` directory within the Neos Install Directory. Contains the main game logs. | 9 | | Libraries Directory | A `Libraries` directory within the Neos Install Directory. Plugins dlls go here. 10 | | Data Directory | Contains the local db, Unity's player.log, and local assets directory. Location can be changed with `-DataPath ` argument. | 11 | | Temporary Directory | Contains crash logs and the cache | 12 | | Cache Directory | Contains cached remote assets. Located inside the Temporary Directory by default. Location can be changed with `-CachePath ` argument. | 13 | 14 | ## Windows 15 | 16 | | Description | Typical Path | 17 | | ----------- | ------------ | 18 | | Neos Install Directory (Steam) | `C:\Program Files (x86)\Steam\steamapps\common\NeosVR` | 19 | | Neos Install Directory (Standalone) | `C:\Neos\app` | 20 | | Data Directory | `%userprofile%\AppData\LocalLow\Solirax\NeosVR` | 21 | | Temporary Directory | `%temp%\Solirax\NeosVR` | 22 | | Cache Directory | `%temp%\Solirax\NeosVR\Cache` | 23 | 24 | ## Linux Native 25 | 26 | | Description | Typical Path | 27 | | ----------- | ------------ | 28 | | Neos Install Directory (Steam) | `$HOME/.local/share/Steam/steamapps/common/NeosVR` | 29 | | Neos Install Directory (Standalone) | *unknown* | 30 | | Data Directory | `$HOME/.config/unity3d/Solirax/NeosVR` | 31 | | Temporary Directory | `/tmp/Solirax/NeosVR` | 32 | | Cache Directory | `/tmp/Solirax/NeosVR/Cache` | 33 | 34 | ## Linux Proton/WINE 35 | 36 | | Description | Typical Path | 37 | | ----------- | ------------ | 38 | | Neos Install Directory (Steam) | `$HOME/.local/share/Steam/steamapps/common/NeosVR` | 39 | | Neos Install Directory (Standalone) | *unknown* | 40 | | Data Directory | `$HOME/.local/share/Steam/steamapps/compatdata/740250/pfx/drive_c/users/steamuser/AppData/LocalLow/Solirax/NeosVR` | 41 | | Temporary Directory | `$HOME/.local/share/Steam/steamapps/compatdata/740250/pfx/drive_c/users/steamuser/Temp/Solirax/NeosVR` | 42 | | Cache Directory | `$HOME/.local/share/Steam/steamapps/compatdata/740250/pfx/drive_c/users/steamuser/Temp/Solirax/NeosVR/Cache` | 43 | 44 | ## Drive Notes 45 | 46 | - The actual Neos install should be less than 1GB, but the log files can in certain cases be very large. 47 | - The cache can get very large, upwards of 30GB so make sure the drive you save cache to has plenty of space. Neos will benefit by having this on a faster drive (read: SSD). The cache directory can be deleted whenever you need without breaking Neos. The cache directory can be changed with the `-CachePath ` launch option. 48 | - The data directory contains your localDB as well as locally saved assets. This can get to be around 10GB, or more if you store a lot in your local home. The data directory can be changed with the `-DataPath ` launch option. Deleting this will: 49 | - Reset any non-cloud-synced Neos settings. This will, for example, send you back to the tutorial (unless you use `-SkipIntroTutorial`) 50 | - Reset your cloud home and nuke anything that was stored in it. 51 | - Regenerate your machine ID 52 | -------------------------------------------------------------------------------- /doc/example_log.log: -------------------------------------------------------------------------------- 1 | 6:36:12 AM.775 ( -1 FPS) Loading Stereo Display 2 | 6:36:12 AM.778 ( -1 FPS) Platform: WindowsPlayer, loaded VR device: Loaded device name: , StereoRenderingMode: MultiPass 3 | 6:36:13 AM.146 ( 0 FPS) Initializing Neos: Beta 2021.11.5.486 4 | CPU: AMD Ryzen 5 5600X 6-Core Processor , Processor Count: 12 (Physical: UNKNOWN) 5 | Max GC Generation: 0, IsLittleEndian: True 6 | System Memory: 31.93 GB 7 | GPU: NVIDIA GeForce RTX 3090, VRAM: 23.78 GB, POT Byte Aligned: True 8 | System.Numerics.Vectors HW accelerated: False, Vector.Count: 4 9 | HeadDevice: Screen 10 | 6:36:13 AM.146 ( 0 FPS) 5.11.0 (Visual Studio built mono) 11 | 6:36:13 AM.146 ( 0 FPS) Supported Texture Formats: Alpha8, RGB24, ARGB32, RGBA32, BGRA32, RGBAHalf, RGBAFloat, BC1, BC3, BC4, BC5, BC6H, BC7 12 | 6:36:13 AM.147 ( 0 FPS) Argument: Neos.exe 13 | 6:36:13 AM.147 ( 0 FPS) Argument: -Screen 14 | 6:36:13 AM.147 ( 0 FPS) Argument: -DontAutoOpenCloudHome 15 | 6:36:13 AM.147 ( 0 FPS) Argument: -BackgroundWorkers 16 | 6:36:13 AM.147 ( 0 FPS) Argument: 6 17 | 6:36:13 AM.147 ( 0 FPS) Argument: -PriorityWorkers 18 | 6:36:13 AM.147 ( 0 FPS) Argument: 5 19 | 6:36:13 AM.147 ( 0 FPS) Argument: -CachePath 20 | 6:36:13 AM.147 ( 0 FPS) Argument: G:\neos 21 | 6:36:13 AM.147 ( 0 FPS) Argument: -LoadAssembly 22 | 6:36:13 AM.147 ( 0 FPS) Argument: Plugins\NeosModLoader.dll 23 | 6:36:13 AM.147 ( 0 FPS) Argument: -Invisible 24 | 6:36:13 AM.321 ( 0 FPS) POSTX Processed Assembly: G:\neos\NeosProLauncher\app\Plugins\NeosModLoader.dll 25 | 6:36:13 AM.383 ( 0 FPS) POSTX Processed Assembly: G:\neos\NeosProLauncher\app\Plugins\NeosDynamicBoneWizard.dll 26 | 6:36:13 AM.389 ( 0 FPS) Available locales: cs, de, en, en-gb, eo, es, et, fi, fr, is, ja, ko, nl, no, pl, ru, sv, tr, zh-cn, zh-tw 27 | 6:36:13 AM.393 ( 0 FPS) Parsing Config file: Config.json 28 | 6:36:13 AM.448 ( 0 FPS) Trigger Deadzone: 0 29 | 6:36:13 AM.481 ( 0 FPS) Loaded Extra Assembly: G:\neos\NeosProLauncher\app\Plugins\NeosModLoader.dll 30 | 6:36:13 AM.484 ( 0 FPS) Compatibility Hash: yjvkwuhbkW7W4MWAI34Zcg== 31 | 6:36:14 AM.061 ( 0 FPS) FreeImage Version: 3.18.0 32 | 6:36:14 AM.061 ( 0 FPS) BepuPhysics Version: 2.4.0-neos3 33 | 6:36:14 AM.061 ( 0 FPS) FreeType Version: 2.10.4 34 | 6:36:14 AM.061 ( 0 FPS) Opus Version: libopus 1.3.1-138-g07376903 35 | 6:36:14 AM.079 ( 0 FPS) Supported 3D model formats: meshx, 3d, 3ds, 3mf, a3d, ac, ac3d, acc, amf, ase, ask, assbin, b3d, blend, bsp, bvh, cob, csm, dae, dxf, enff, fbx, glb, gltf, hmp, ifc, ifczip, irr, irrmesh, lwo, lws, lxo, m3d, md2, md3, md5anim, md5camera, md5mesh, mdc, mdl, mesh, mesh.xml, mot, ms3d, ndo, nff, obj, off, ogex, pk3, ply, pmx, prj, q3o, q3s, raw, scn, sib, smd, step, stl, stp, ter, uc, vta, x, xgl, xml, zae, zgl 36 | 6:36:14 AM.079 ( 0 FPS) Supported point cloud formats: pts, las, laz 37 | 6:36:14 AM.079 ( 0 FPS) Supported image formats: bmp, ico, jpg, jif, jpeg, jpe, jng, koa, iff, lbm, mng, pbm, pcd, pcx, pgm, png, ppm, ras, tga, targa, tif, tiff, wap, wbmp, wbm, psd, psb, cut, xbm, xpm, dds, gif, hdr, g3, sgi, rgb, rgba, bw, exr, j2k, j2c, jp2, pfm, pct, pict, pic, 3fr, arw, bay, bmq, cap, cine, cr2, crw, cs1, dc2, dcr, drf, dsc, dng, erf, fff, ia, iiq, k25, kc2, kdc, mdc, mef, mos, mrw, nef, nrw, orf, pef, ptx, pxn, qtk, raf, raw, rdc, rw2, rwl, rwz, sr2, srf, srw, sti, x3f, webp, jxr, wdp, hdp 38 | 6:36:14 AM.079 ( 0 FPS) Supported audio formats: wav, wave, flac, fla, ogg, aiff, aif, aifc 39 | 6:36:14 AM.079 ( 0 FPS) Supported video formats: mp4, mpeg, avi, mov, mpg, mkv, flv, webm, mts, 3gp, bik, m2v, m2s, wmv, m3u8, m3u, pls, m4a, mp3, mpeg3, aac, ac3, aif, aiff, ape, au, it, mka, mod, mp1, mp2, opus, s3m, sid, w64, wma, xm, nsf, nsfe, gbs, vgm, vgz, spc, gym 40 | 6:36:14 AM.079 ( 0 FPS) Supported font formats: ttf, otf, ttc, otc, woff 41 | 6:36:14 AM.079 ( 0 FPS) Supported subtitle formats: srt, sub, sub, ssa, ttml, vtt 42 | 6:36:14 AM.438 ( 0 FPS) [INFO] [NeosModLoader] NeosModLoader v1.5.0 starting up! 43 | 6:36:14 AM.572 ( 0 FPS) [INFO] [NeosModLoader] version spoofing succeeded 44 | 6:36:14 AM.585 ( 0 FPS) [INFO] [NeosModLoader] loaded mod BetterLogixWires 1.0.1 from G:\neos\NeosProLauncher\app\nml_mods\BetterLogixWires.dll 45 | 6:36:14 AM.747 ( 0 FPS) [INFO] [NeosModLoader] loaded mod ColorDrop 1.0.0 from G:\neos\NeosProLauncher\app\nml_mods\ColorDrop.dll 46 | 6:36:14 AM.760 ( 0 FPS) [INFO] [NeosModLoader] loaded mod ExportNeosToJson 1.1.1 from G:\neos\NeosProLauncher\app\nml_mods\ExportNeosToJson.dll 47 | 6:36:14 AM.763 ( 0 FPS) [INFO] [NeosModLoader/ExportNeosToJson] Hook installed successfully 48 | 6:36:14 AM.765 ( 0 FPS) [INFO] [NeosModLoader] loaded mod InspectorScroll 1.0.0 from G:\neos\NeosProLauncher\app\nml_mods\InspectorScroll.dll 49 | 6:36:14 AM.818 ( 0 FPS) [INFO] [NeosModLoader] loaded mod MotionBlurDisable 1.1.0 from G:\neos\NeosProLauncher\app\nml_mods\MotionBlurDisable.dll 50 | 6:36:14 AM.820 ( 0 FPS) [INFO] [NeosModLoader/MotionBlurDisable] Hook installed successfully 51 | 6:36:14 AM.822 ( 0 FPS) [INFO] [NeosModLoader/MotionBlurDisable] disabled 1 prexisting motion blurs 52 | 6:36:14 AM.828 ( 0 FPS) [INFO] [NeosModLoader] loaded mod NeosContactsSort 1.0.0 from G:\neos\NeosProLauncher\app\nml_mods\NeosContactsSort.dll 53 | 6:36:14 AM.849 ( 0 FPS) [INFO] [NeosModLoader/NeosContactsSort] Hooks installed successfully! 54 | 6:36:14 AM.849 ( 0 FPS) [INFO] [NeosModLoader] loaded mod NeosLocomotionRename 1.0.0 from G:\neos\NeosProLauncher\app\nml_mods\NeosLocomotionRename.dll 55 | 6:36:14 AM.851 ( 0 FPS) [INFO] [NeosModLoader/NeosLocomotionRename] LocomotionModule patch installed successfully 56 | 6:36:14 AM.852 ( 0 FPS) [INFO] [NeosModLoader/NeosLocomotionRename] NoclipLocomotion patch installed successfully 57 | 6:36:14 AM.852 ( 0 FPS) [INFO] [NeosModLoader/NeosLocomotionRename] GrabWorldLocomotion patch installed successfully 58 | 6:36:14 AM.852 ( 0 FPS) [INFO] [NeosModLoader/NeosLocomotionRename] SlideLocomotion patch installed successfully 59 | 6:36:14 AM.853 ( 0 FPS) [INFO] [NeosModLoader/NeosLocomotionRename] TeleportLocomotion patch installed successfully 60 | 6:36:14 AM.853 ( 0 FPS) [INFO] [NeosModLoader] loaded mod NeosTrackerIdStabilizer 1.0.0 from G:\neos\NeosProLauncher\app\nml_mods\NeosTrackerIdStabilizer.dll 61 | 6:36:14 AM.855 ( 0 FPS) [INFO] [NeosModLoader/NeosTrackerIdStabilizer] patch installed successfully 62 | 6:36:14 AM.855 ( 0 FPS) [INFO] [NeosModLoader] loaded mod ShowComponentSlot 1.0.0 from G:\neos\NeosProLauncher\app\nml_mods\ShowComponentSlot.dll 63 | 6:36:14 AM.864 ( 0 FPS) [INFO] [NeosModLoader] loaded mod ShowDriveSource 1.1.1 from G:\neos\NeosProLauncher\app\nml_mods\ShowDriveSource.dll 64 | 6:36:14 AM.867 ( 0 FPS) [INFO] [NeosModLoader] loaded mod ToolshelfAnarchy 1.0.0 from G:\neos\NeosProLauncher\app\nml_mods\ToolshelfAnarchy.dll 65 | 6:36:14 AM.868 ( 0 FPS) [INFO] [NeosModLoader] loaded mod VoiceVolumeOverride 1.0.1 from G:\neos\NeosProLauncher\app\nml_mods\VoiceVolumeOverride.dll 66 | 6:36:14 AM.937 ( 0 FPS) [INFO] [NeosModLoader/VoiceVolumeOverride] Patching success! 67 | 6:36:14 AM.966 ( 0 FPS) ExecutionHook: static constructor called! 68 | 6:36:15 AM.269 ( 0 FPS) Reading settings file 69 | 6:36:15 AM.286 ( 0 FPS) Performing version checks and updates, loaded version: 8 70 | 6:36:15 AM.287 ( 0 FPS) MachineID: gu2p8llwpuozuqfy84byaa 71 | 6:36:15 AM.437 ( 0 FPS) Configuring LiteDB database 72 | 6:36:15 AM.521 ( 0 FPS) Saving Settings 73 | 6:36:15 AM.534 ( 0 FPS) [INFO] [NeosModLoader/VoiceVolumeOverride] Loaded voice multiplier setting from localdb 74 | 6:36:15 AM.548 ( 0 FPS) Initialized WorkProcessor. Background Workers: 6, Priority Workers: 5 75 | 6:36:15 AM.586 ( 0 FPS) Graphics Device Type: Direct3D11 76 | 6:36:15 AM.612 ( 0 FPS) AudioSystem SampleRate: 48000, BufferLength: 1024, NumBuffers: 4 77 | 6:36:15 AM.662 ( 0 FPS) HttpClient AutomaticDecompressionSupported: True 78 | 6:36:15 AM.695 ( 0 FPS) Forcing Invisible status 79 | 6:36:15 AM.695 ( 0 FPS) DoNotSendReadStatus set to: False 80 | 6:36:15 AM.726 ( 0 FPS) Executable: G:\neos\NeosProLauncher\app\RuntimeData\Neosmon\NeosmonServer.exe 81 | 6:36:15 AM.812 ( 0 FPS) Local User SteamID: 63563198443297593 82 | 6:36:15 AM.812 ( 0 FPS) SteamAPI initialized 83 | 6:36:15 AM.813 ( 0 FPS) Steam Voice Initialized 84 | 6:36:15 AM.844 ( 0 FPS) Supported network protocols: lnl-nat, lnl, neos-steam 85 | 6:36:15 AM.844 ( 0 FPS) FrooxEngine Initialized in 2699.2908 ms 86 | 6:36:15 AM.845 ( 0 FPS) Initializing CSCore Audio Input Driver 87 | 6:36:15 AM.873 ( 0 FPS) DefaultCapture: Microphone (Yeti Stereo Microphone), ID: {0.0.1.00000000}.{d5f2c31b-610d-458b-beaf-ee6afaa2c5eb} 88 | 6:36:15 AM.873 ( 0 FPS) DefaultOutput: Speakers (Realtek(R) Audio), ID: {0.0.0.00000000}.{c7acf5e4-16a7-4b61-b982-75c0604319ff} 89 | 6:36:15 AM.878 ( 0 FPS) Initialized Audio Input MMDevice Line 1 (Virtual Audio Cable), ID: {0.0.0.00000000}.{2bf5cf6e-a0a3-45b4-9e63-fb3e47d1dbe4}, Format: ChannelsAvailable: 2|SampleRate: 48000|Bps: 192000|BlockAlign: 4|BitsPerSample: 16|Encoding: Extensible|SubFormat: 00000001-0000-0010-8000-00aa00389b71|ChannelMask: SpeakerFrontLeft, SpeakerFrontRight, Default: False 90 | 6:36:15 AM.878 ( 0 FPS) Initialized Audio Input MMDevice Speakers (Realtek(R) Audio), ID: {0.0.0.00000000}.{c7acf5e4-16a7-4b61-b982-75c0604319ff}, Format: ChannelsAvailable: 2|SampleRate: 48000|Bps: 384000|BlockAlign: 8|BitsPerSample: 32|Encoding: Extensible|SubFormat: 00000001-0000-0010-8000-00aa00389b71|ChannelMask: SpeakerFrontLeft, SpeakerFrontRight, Default: True 91 | 6:36:15 AM.879 ( 0 FPS) Initialized Audio Input MMDevice Line 1 (Virtual Audio Cable), ID: {0.0.1.00000000}.{3b0d1771-e3fe-4fea-9ec0-c06fcd85b6ed}, Format: ChannelsAvailable: 2|SampleRate: 48000|Bps: 192000|BlockAlign: 4|BitsPerSample: 16|Encoding: Extensible|SubFormat: 00000001-0000-0010-8000-00aa00389b71|ChannelMask: SpeakerFrontLeft, SpeakerFrontRight, Default: False 92 | 6:36:15 AM.879 ( 0 FPS) Initialized Audio Input MMDevice Digital Audio Interface (Valve VR Radio & HMD Mic), ID: {0.0.1.00000000}.{b4c854a8-c21b-45a6-9b3a-ea366b2d600c}, Format: ChannelsAvailable: 1|SampleRate: 48000|Bps: 96000|BlockAlign: 2|BitsPerSample: 16|Encoding: Extensible|SubFormat: 00000001-0000-0010-8000-00aa00389b71|ChannelMask: SpeakerFrontCenter, Default: False 93 | 6:36:15 AM.880 ( 0 FPS) Initialized Audio Input MMDevice Microphone (Yeti Stereo Microphone), ID: {0.0.1.00000000}.{d5f2c31b-610d-458b-beaf-ee6afaa2c5eb}, Format: ChannelsAvailable: 2|SampleRate: 48000|Bps: 192000|BlockAlign: 4|BitsPerSample: 16|Encoding: Extensible|SubFormat: 00000001-0000-0010-8000-00aa00389b71|ChannelMask: SpeakerFrontLeft, SpeakerFrontRight, Default: True 94 | 6:36:15 AM.903 ( 0 FPS) Touch injection driver initialized 95 | 6:36:15 AM.981 ( 0 FPS) Registered displays: 96 | Display 0, Resolution: [2560; 1440], Rate: -1.00 Hz, Orientation: Default, DPI: [123.0; 123.0], Offset: [0; 0], Primary: True 97 | Display 1, Resolution: [1920; 1080], Rate: -1.00 Hz, Orientation: Default, DPI: [93.0; 93.0], Offset: [397; -1080], Primary: False 98 | 6:36:15 AM.983 ( 0 FPS) Initializing bHaptics... 99 | 6:36:16 AM.043 ( 0 FPS) [INFO] [NeosModLoader/MotionBlurDisable] Hook triggered! Everything worked! 100 | 6:36:16 AM.625 ( 0 FPS) Starting running world: 101 | 6:36:16 AM.802 ( 0 FPS) Running default bootstrap 102 | 6:36:18 AM.286 ( 0 FPS) User totally-not-runtime Role: , HasFingerTracking: False, HasEyeTracking: False, HasLipTracking: False 103 | 6:36:19 AM.606 ( 0 FPS) Has LeftTool: True, Has RightTool: True 104 | 6:36:20 AM.058 ( 0 FPS) Setting URL: local://gu2p8llwpuozuqfy84byaa/TgI-LTb1aUGxn3g-oVeJtA.lz4bson 105 | 6:36:20 AM.215 ( 0 FPS) DoNotSendReadStatus set to: False 106 | 6:36:20 AM.307 ( 0 FPS) NetworkInitStart 107 | 6:36:20 AM.344 ( 0 FPS) Creating Listener for host SteamID 76561198048877596, channel: 0, sessionId: S-ee273f23-4445-446b-9cb4-559b14d98632 108 | 6:36:20 AM.346 ( 0 FPS) Starting running world: 109 | 6:36:21 AM.002 ( 0 FPS) DoNotSendReadStatus set to: False 110 | 6:36:21 AM.415 ( 0 FPS) User Joined Userspace. Username: totally-not-runtime, UserID: , AllocID: 0, AllocIDstart: 1, MachineID: gu2p8llwpuozuqfy84byaa 111 | 6:36:21 AM.417 ( 0 FPS) User Spawn Userspace. Username: totally-not-runtime, UserID: , MachineID: gu2p8llwpuozuqfy84byaa 112 | 6:36:21 AM.821 ( 0 FPS) Initializing SignalR 113 | 6:36:21 AM.953 ( 0 FPS) Connecting to SignalR... 114 | 6:36:22 AM.150 ( 0 FPS) Loading workspace Private/RadiantDash/Screens from: neosrec:///U-runtime/Workspaces/Private/RadiantDash/Screens 115 | 6:36:22 AM.151 ( 0 FPS) Loading workspace Private/RadiantDash/TopBar from: neosrec:///U-runtime/Workspaces/Private/RadiantDash/TopBar 116 | 6:36:22 AM.662 ( 0 FPS) Got workspace asset file: G:\neos\Cache\nrj0py9nkvd6glgj 117 | 6:36:22 AM.843 ( 0 FPS) Got workspace asset file: G:\neos\Cache\q84p4g3fzcy16g6f 118 | 6:36:23 AM.036 ( 0 FPS) Unresolved count: 35, Processed in: 00:00:00.0025998 119 | 6:36:23 AM.036 ( 0 FPS) World Local Loaded in: 00:00:00.6481973 120 | 6:36:23 AM.048 ( 0 FPS) User Joined Local. Username: totally-not-runtime, UserID: , AllocID: 0, AllocIDstart: 1, MachineID: gu2p8llwpuozuqfy84byaa 121 | 6:36:23 AM.048 ( 0 FPS) User Spawn Local. Username: totally-not-runtime, UserID: , MachineID: gu2p8llwpuozuqfy84byaa 122 | 6:36:23 AM.048 ( 0 FPS) Spawning User totally-not-runtime (ID2C00) 123 | 6:36:23 AM.067 ( 0 FPS) User totally-not-runtime Role: Admin, HasFingerTracking: False, HasEyeTracking: False, HasLipTracking: False 124 | 6:36:23 AM.244 ( 0 FPS) Starting GatherJob for https://cloudxthumbnails.azureedge.net/7ccf494e-e413-4d2b-993c-a2f48babe44e-v2.webp, AttemptsLeft: 5 125 | 6:36:23 AM.648 ( 1 FPS) Loaded workspace Private/RadiantDash/TopBar 126 | 6:36:23 AM.664 ( 1 FPS) GatherJob for https://cloudxthumbnails.azureedge.net/7ccf494e-e413-4d2b-993c-a2f48babe44e-v2.webp FAILED! Reason: NotFound 127 | 6:36:23 AM.664 ( 1 FPS) Failed Load: Could not gather asset variant: , file: , for: https://cloudxthumbnails.azureedge.net/7ccf494e-e413-4d2b-993c-a2f48babe44e-v2.webp, instance: -655408640 128 | 6:36:24 AM.566 ( 1 FPS) Loaded workspace Private/RadiantDash/Screens 129 | 6:36:25 AM.173 ( 1 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/376139c7-cb68-423e-a8f3-1fb1d7cee189-v2.webp, AttemptsLeft: 5 130 | 6:36:25 AM.213 ( 1 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/f0bb17f3-dfa2-4334-8a87-4ba16e4d43a7-v2.webp, AttemptsLeft: 5 131 | 6:36:25 AM.223 ( 1 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/054431e3-e61e-4260-a6ff-aff519ec46ef-v2.webp, AttemptsLeft: 5 132 | 6:36:25 AM.227 ( 1 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/b613c6b3-c206-418a-b362-ebd18c1b093d-v2.webp, AttemptsLeft: 5 133 | 6:36:25 AM.231 ( 1 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/0134dfb3-6ae0-41cb-8efd-300a5446d564-v2.webp, AttemptsLeft: 5 134 | 6:36:25 AM.241 ( 1 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/ccb91a14-261f-4f2f-8222-6b9429edbed8-v2.webp, AttemptsLeft: 5 135 | 6:36:25 AM.245 ( 1 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/131c3678-12de-486f-9ea5-f10b7ce01673-v2.webp, AttemptsLeft: 5 136 | 6:36:25 AM.248 ( 1 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/42e9425d-0871-4d32-88bc-2e4b44ffcfbc-v2.webp, AttemptsLeft: 5 137 | 6:36:25 AM.383 ( 1 FPS) OldVersion: 1, NewVersion: 1, Modified: True 138 | 6:36:25 AM.383 ( 1 FPS) OldVersion: 2, NewVersion: 2, Modified: True 139 | 6:36:25 AM.413 ( 1 FPS) bHaptics detected - Head: False, Vest: False, LeftForearm: False, RightForearm: False, LeftFoot: False, RightFoot: False 140 | 6:36:25 AM.420 ( 1 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/376139c7-cb68-423e-a8f3-1fb1d7cee189-v2.webp. Elapsed: 0.2475513 s 141 | 6:36:25 AM.421 ( 1 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/054431e3-e61e-4260-a6ff-aff519ec46ef-v2.webp. Elapsed: 0.1980411 s 142 | 6:36:25 AM.423 ( 1 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/f0bb17f3-dfa2-4334-8a87-4ba16e4d43a7-v2.webp. Elapsed: 0.2100441 s 143 | 6:36:25 AM.442 ( 1 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/42e9425d-0871-4d32-88bc-2e4b44ffcfbc-v2.webp. Elapsed: 0.1942911 s 144 | 6:36:25 AM.453 ( 1 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/131c3678-12de-486f-9ea5-f10b7ce01673-v2.webp. Elapsed: 0.2077937 s 145 | 6:36:25 AM.492 ( 1 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/0134dfb3-6ae0-41cb-8efd-300a5446d564-v2.webp. Elapsed: 0.2603044 s 146 | 6:36:25 AM.519 ( 1 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/b613c6b3-c206-418a-b362-ebd18c1b093d-v2.webp. Elapsed: 0.2914099 s 147 | 6:36:25 AM.534 ( 1 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/ccb91a14-261f-4f2f-8222-6b9429edbed8-v2.webp. Elapsed: 0.2929102 s 148 | 6:36:25 AM.679 ( 1 FPS) Connected to SignalR 149 | 6:36:25 AM.718 ( 14 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/0eee52f8-9427-4849-93e5-df2a769864f1-v2.webp, AttemptsLeft: 5 150 | 6:36:25 AM.732 ( 14 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/3970da89-abf5-4ac4-a0a5-dfd012ad0698-v2.webp, AttemptsLeft: 5 151 | 6:36:25 AM.746 ( 14 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/2624049e-679e-418f-9ec7-5bc3fb517a87-v2.webp, AttemptsLeft: 5 152 | 6:36:25 AM.793 ( 14 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/3970da89-abf5-4ac4-a0a5-dfd012ad0698-v2.webp. Elapsed: 0.0607632 s 153 | 6:36:25 AM.805 ( 14 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/b6f5cf4a-6e2e-41de-9c3f-ce912d2a7939-v2.webp, AttemptsLeft: 5 154 | 6:36:25 AM.832 ( 14 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/fad39e61-f6a8-493a-a172-ba3345e0e8ac-v2.webp, AttemptsLeft: 5 155 | 6:36:25 AM.844 ( 14 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/2624049e-679e-418f-9ec7-5bc3fb517a87-v2.webp. Elapsed: 0.0982707 s 156 | 6:36:25 AM.864 ( 14 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/0eee52f8-9427-4849-93e5-df2a769864f1-v2.webp. Elapsed: 0.1462799 s 157 | 6:36:25 AM.868 ( 14 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/16b8dcfc-b517-436f-9841-cde3b28634a2-v2.webp, AttemptsLeft: 5 158 | 6:36:25 AM.883 ( 14 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/b5d0df72-da46-4f92-ac82-00f4cfd67c09-v2.webp, AttemptsLeft: 5 159 | 6:36:25 AM.886 ( 14 FPS) Starting GatherJob for neosdb:///a1df5d0574267538ce010cdfdc534752bbbd0bdf11f3e7327855050be5be95d6?version=2&compression=BC1_Crunched&quality=100&width=128&height=64&mips=True&filtering=Box, AttemptsLeft: 5 160 | 6:36:25 AM.933 ( 14 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/b5d0df72-da46-4f92-ac82-00f4cfd67c09-v2.webp. Elapsed: 0.0502606 s 161 | 6:36:25 AM.941 ( 14 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/b6f5cf4a-6e2e-41de-9c3f-ce912d2a7939-v2.webp. Elapsed: 0.1365287 s 162 | 6:36:25 AM.953 ( 14 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/fad39e61-f6a8-493a-a172-ba3345e0e8ac-v2.webp. Elapsed: 0.1215261 s 163 | 6:36:25 AM.991 ( 14 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/16b8dcfc-b517-436f-9841-cde3b28634a2-v2.webp. Elapsed: 0.1222756 s 164 | 6:36:26 AM.098 ( 14 FPS) Finished Gather for: neosdb:///a1df5d0574267538ce010cdfdc534752bbbd0bdf11f3e7327855050be5be95d6?version=2&compression=BC1_Crunched&quality=100&width=128&height=64&mips=True&filtering=Box. Elapsed: 0.2107945 s 165 | 6:36:26 AM.275 (164 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/e1ecfd12-e717-44dd-af2d-d8a811db8f0c-v2.webp, AttemptsLeft: 5 166 | 6:36:26 AM.297 (164 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/180bb6ac-9552-4a91-b7cc-302bcf4d85ec-v2.webp, AttemptsLeft: 5 167 | 6:36:26 AM.334 (164 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/e1ecfd12-e717-44dd-af2d-d8a811db8f0c-v2.webp. Elapsed: 0.0585121 s 168 | 6:36:26 AM.344 (164 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/180bb6ac-9552-4a91-b7cc-302bcf4d85ec-v2.webp. Elapsed: 0.0472601 s 169 | 6:36:26 AM.667 (164 FPS) Starting GatherJob for neosdb:///8611b396369b31a6d54dd374c65e1733414b4e5cf3921db9d7c7a66cb03752ce.7zbson, AttemptsLeft: 5 170 | 6:36:26 AM.670 (164 FPS) Starting GatherJob for neosdb:///a1df5d0574267538ce010cdfdc534752bbbd0bdf11f3e7327855050be5be95d6?version=2&compression=BC1_Crunched&quality=100&width=256&height=128&mips=False&filtering=Box, AttemptsLeft: 5 171 | 6:36:26 AM.862 (156 FPS) Finished Gather for: neosdb:///8611b396369b31a6d54dd374c65e1733414b4e5cf3921db9d7c7a66cb03752ce.7zbson. Elapsed: 0.1942913 s 172 | 6:36:26 AM.930 (156 FPS) DoNotSendReadStatus set to: False 173 | 6:36:27 AM.065 (156 FPS) Finished Gather for: neosdb:///a1df5d0574267538ce010cdfdc534752bbbd0bdf11f3e7327855050be5be95d6?version=2&compression=BC1_Crunched&quality=100&width=256&height=128&mips=False&filtering=Box. Elapsed: 0.3958375 s 174 | 6:36:27 AM.145 (156 FPS) Starting GatherJob for neosdb:///a1df5d0574267538ce010cdfdc534752bbbd0bdf11f3e7327855050be5be95d6?version=2&compression=BC1_Crunched&quality=100&width=512&height=256&mips=False&filtering=Box, AttemptsLeft: 5 175 | 6:36:27 AM.196 (105 FPS) Finished Gather for: neosdb:///a1df5d0574267538ce010cdfdc534752bbbd0bdf11f3e7327855050be5be95d6?version=2&compression=BC1_Crunched&quality=100&width=512&height=256&mips=False&filtering=Box. Elapsed: 0.0510112 s 176 | 6:36:27 AM.204 (105 FPS) Starting GatherJob for neosdb:///a1df5d0574267538ce010cdfdc534752bbbd0bdf11f3e7327855050be5be95d6?version=2&compression=BC1_Crunched&quality=100&width=1024&height=512&mips=False&filtering=Box, AttemptsLeft: 5 177 | 6:36:27 AM.259 (105 FPS) Finished Gather for: neosdb:///a1df5d0574267538ce010cdfdc534752bbbd0bdf11f3e7327855050be5be95d6?version=2&compression=BC1_Crunched&quality=100&width=1024&height=512&mips=False&filtering=Box. Elapsed: 0.0545946 s 178 | 6:37:23 AM.982 (165 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/0558e36f-2982-4a81-94d2-1471795a368a-v2.webp, AttemptsLeft: 5 179 | 6:37:23 AM.985 (165 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/59f470a9-2805-41af-9620-64b78006b6a6-v2.webp, AttemptsLeft: 5 180 | 6:37:23 AM.990 (165 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/81e3a54e-f9b6-43e9-aa79-86ab1c1b9e8a-v2.webp, AttemptsLeft: 5 181 | 6:37:23 AM.992 (165 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/aacf4d8e-3a65-450f-86ea-f6f1017ba972-v2.webp, AttemptsLeft: 5 182 | 6:37:23 AM.995 (165 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/a565d32e-9100-4bcf-8c61-4c98fd2ef304-v2.webp, AttemptsLeft: 5 183 | 6:37:23 AM.996 (165 FPS) Starting GatherJob for https://operationaldata.neos.com/thumbnails/056dfcee-c6d8-4a13-99dd-b7653be1a17b-v2.webp, AttemptsLeft: 5 184 | 6:37:24 AM.038 (165 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/0558e36f-2982-4a81-94d2-1471795a368a-v2.webp. Elapsed: 0.0557797 s 185 | 6:37:24 AM.041 (165 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/59f470a9-2805-41af-9620-64b78006b6a6-v2.webp. Elapsed: 0.0557799 s 186 | 6:37:24 AM.043 (165 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/aacf4d8e-3a65-450f-86ea-f6f1017ba972-v2.webp. Elapsed: 0.0505283 s 187 | 6:37:24 AM.104 (165 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/81e3a54e-f9b6-43e9-aa79-86ab1c1b9e8a-v2.webp. Elapsed: 0.1143501 s 188 | 6:37:24 AM.134 (165 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/056dfcee-c6d8-4a13-99dd-b7653be1a17b-v2.webp. Elapsed: 0.1383553 s 189 | 6:37:24 AM.142 (165 FPS) Finished Gather for: https://operationaldata.neos.com/thumbnails/a565d32e-9100-4bcf-8c61-4c98fd2ef304-v2.webp. Elapsed: 0.1466071 s 190 | 6:37:41 AM.835 (165 FPS) Exiting Neos. Save Homes: False 191 | 6:37:41 AM.908 (165 FPS) Starting running world: 192 | 6:37:41 AM.931 (143 FPS) User Joined runtime World 1. Username: runtime, UserID: U-runtime, AllocID: 0, AllocIDstart: 1, MachineID: gu2p8llwpuozuqfy84byaa 193 | 6:37:41 AM.931 (143 FPS) User Spawn runtime World 1. Username: runtime, UserID: U-runtime, MachineID: gu2p8llwpuozuqfy84byaa 194 | 6:37:41 AM.931 (143 FPS) Spawning User runtime (ID2C00) 195 | 6:37:41 AM.931 (143 FPS) User runtime Role: Admin, HasFingerTracking: False, HasEyeTracking: False, HasLipTracking: False 196 | 6:37:42 AM.126 (143 FPS) Exception running asynchronous task: 197 | System.AggregateException: One or more errors occurred. ---> System.NullReferenceException: Object reference not set to an instance of an object 198 | at FrooxEngine.WorldListManager+d__88.MoveNext () [0x00a93] in :0 199 | --- End of inner exception stack trace --- 200 | ---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object 201 | at FrooxEngine.WorldListManager+d__88.MoveNext () [0x00a93] in :0 <--- 202 | 203 | 204 | at void BaseX.UniLog.Error(string message, bool stackTrace) 205 | at void FrooxEngine.CoroutineManager.CheckExceptions(Task task) 206 | at void System.Threading.Tasks.ContinuationTaskFromTask.InnerInvoke() 207 | at void System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() 208 | at bool System.Threading.ThreadPoolWorkQueue.Dispatch() 209 | at bool System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() 210 | 6:37:42 AM.247 (143 FPS) Starting sync for U-runtime:R-Settings. Local: 476, Global: 475 211 | 6:37:44 AM.352 (151 FPS) Finished sync for U-runtime:R-Settings. Local: 476, Global: 475 212 | 6:37:44 AM.360 (151 FPS) Shutdown requested 213 | 6:37:44 AM.360 (151 FPS) Shutting down 214 | 6:37:44 AM.588 (151 FPS) Requesting environment shutdown 215 | 6:37:44 AM.590 (151 FPS) Shutting down environment 216 | 6:37:44 AM.933 (151 FPS) Saving Settings 217 | -------------------------------------------------------------------------------- /doc/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## Something is broken! Where can I get help? 4 | 5 | Please take a look at our [troubleshooting page](troubleshooting.md). 6 | 7 | ## Do you have a Discord server? 8 | 9 | Yes. [Here it is.](https://discord.gg/vCDJK9xyvm) 10 | 11 | ## What is a mod? 12 | 13 | Mods are .dll files loaded by NeosModLoader that change the behavior of your Neos client in some way. Unlike plugins, mods are specifically designed to work in multiplayer. 14 | 15 | ## What does NeosModLoader do? 16 | 17 | NeosModLoader is simply a Neos [plugin](https://wiki.neos.com/Plugins) that does a lot of the boilerplate necessary to get mods working in a reasonable way. In summary, it: 18 | 19 | 1. Initializes earlier than a normal plugin 20 | 2. Ensures that Neos's compatibility check doesn't prevent you from joining other players. For safety reasons this will only work if NeosModLoader is the only plugin. 21 | 3. Loads mod .dll files and calls their `OnEngineInit()` function so the mods can begin executing 22 | 23 | ## Is using NeosModLoader allowed? 24 | 25 | Yes, so long as Neos's [Mod & Plugin Policy] is followed. 26 | 27 | ## Will people know I'm using mods? 28 | 29 | - By default, NeosModLoader does not do anything identifiable over the network. You will appear to be running the vanilla Neos version to any component that shows your version strings or compatibility hash. 30 | - If you are running other plugins, they will alter your version strings and compatibility hash. 31 | - NeosModLoader logs to the same log file Neos uses. If you send your logs to anyone it will be obvious that you are using a plugin. This is intended. 32 | - NeosModLoader mods may have effects visible to other users, depending on the mod. 33 | - If you wish to opt in to using your real version string you can set `advertiseversion=true` in the NeosModLoader.config file. 34 | - If NeosModLoader breaks due to a bad install or a Neos update, it will be unable to hide its own existence. 35 | 36 | ## Are mods safe? 37 | 38 | Mods are not sandboxed in any way. In other words, they run with the same level of privilege as Neos itself. A poorly written mod could cause performance or stability issues. A maliciously designed mod could give a malicious actor a dangerous level of control over your computer. **Make sure you only use mods from sources you trust.** 39 | 40 | The modding community maintains [a list of mods](https://www.neosmodloader.com/mods) that have been manually audited to ensure they aren't evil. While this process isn't 100% foolproof, the mods on this list are significantly more trustworthy than an unvetted DLL. 41 | 42 | If you aren't sure if you can trust a mod and you have some level of ability to read code, you can look at its source code. If the source code is unavailable or you suspect it may differ from the contents of the .dll file, you can inspect the mod with a [C# decompiler](https://www.google.com/search?q=c%23+decompiler). Things to be particularly wary of include: 43 | 44 | - Obfuscated code 45 | - Sending or receiving data over the internet 46 | - Interacting with the file system (reading, writing, or executing files from disk) 47 | 48 | ## Where does NeosModLoader log to? 49 | 50 | The regular Neos logs: `C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Logs` 51 | 52 | ## Is NeosModLoader compatible with other mod loaders? 53 | 54 | Yes, **however** other mod loaders are likely to come with LibHarmony, and you need to ensure you only have one. Therefore you may need to remove 0Harmony.dll from your Neos install directory. If the foreign mod loader's LibHarmony version is significantly different from the standard Harmony 2 library, then it will not be compatible with NeosModLoader at all. 55 | 56 | ## Why did you build a custom mod loader for Neos? 57 | 58 | 1. ~~Neos Plugins are given extra protections in the [Neos Guidelines](https://docs.google.com/document/d/1mqdbIvbj1b2LeFhNzfAASeTpRZk6vmbXISYLdTXTVR4/edit), and those same protections are not extended to a generic Unity mod loader.~~ This is no longer true, as modding Neos is now specifically allowed within Neos's [Mod & Plugin Policy]. 59 | 2. As Neos Plugins are officially supported we can expect them to continue working even through major engine changes, for example if Neos ever switches to a non-Unity engine. 60 | 61 | ## As a content creator, when is a mod the right solution? 62 | 63 | Check out this document for more detail: [Problem Solving Techniques](problem_solving_techniques.md). 64 | 65 | ## As a mod developer, why should I use NeosModLoader over a Neos Plugin? 66 | 67 | If you are just trying to make a new component or logix node, you should use a plugin. The plugin system is specifically designed for that. 68 | 69 | If you are trying to modify Neos's existing behavior without adding any new components, NeosModLoader offers the following: 70 | 71 | - [LibHarmony] is a dependency of NeosModLoader, so as a mod developer you don't need to worry about making sure it's installed 72 | - Neos Plugins normally break multiplayer compatibility. The NeosModLoader plugin has been specifically designed to remain compatible. This feature will only work if NeosModLoader.dll is the *only* plugin you are using. 73 | - Neos Plugins can normally execute when Local Home loads at the earliest. NeosModLoader begins executing significantly earlier, giving you more room to alter Neos's behavior before it finishes initializing. 74 | - Steam has a relatively small character limit on launch options, and every Neos plugin you install pushes you closer to that limit. Having more than a handful plugins will therefore prevent you from using Steam to launch the game, and NeosModLoader is unaffected by this issue. 75 | 76 | ## Can mods depend on other mods? 77 | 78 | Yes. All mod assemblies are loaded before any mod hooks are called, so no special setup is needed if your mod provides public methods. 79 | 80 | Mod hooks are called alphabetically by the mod filename, so you can purposefully alter your filename (`0_mod.dll`) to make sure your hooks run first. 81 | 82 | ## Can NeosModLoader load Neos plugins? 83 | 84 | No. You need to use `-LoadAssembly ` to load plugins. There is important plugin handling code that does not run for NeosModLoader mods. 85 | 86 | ## Are NeosModLoader mods plugins? 87 | 88 | No. NeosModLoader mods will not work if used as a Neos plugin. 89 | 90 | 91 | [Mod & Plugin Policy]: https://wiki.neos.com/Mod_%26_Plugin_Policy 92 | -------------------------------------------------------------------------------- /doc/how_nml_works.md: -------------------------------------------------------------------------------- 1 | # Going Into Detail: How NeosModLoader Interfaces with Neos 2 | 3 | NeosModLoader interfaces with Neos in two main places: the hook and the compatibility hash. 4 | 5 | ## The Hook 6 | 7 | The hook is the point where Neos "hooks" into NeosModLoader, allowing it to begin execution. 8 | 9 | Typically, [plugins] use [components] to execute custom code. This is limiting as they can only begin execution once a world loads. This usually involves putting a plugin's component into your local home. 10 | 11 | The NeosModLoader plugin uses a different mechanism. Instead of using a component, it uses a connector. Neos loads connectors during its initial setup, at which point they can begin execution. As this system is independent of world loading it is more reliable. The connector implementation is in [ExecutionHook.cs](../NeosModLoader/ExecutionHook.cs). 12 | 13 | This connector-based hook does not modify the Neos application, and only uses public Neos APIs. 14 | 15 | ## The Compatibility Hash 16 | 17 | [Neos plugins][plugin], when loaded, alter your client's compatibility hash from what a vanilla client would have. You cannot join a session unless your compatibility hash is an exact match with the host. 18 | 19 | Plugins are intended to add new features such as [components] and [LogiX nodes][logix] to the [data model], and altering the data model breaks multiplayer compatibility. This is why the compatibility hash exists, and why plugins alter it. 20 | 21 | NeosModLoader does not change the data model, therefore it is 100% network compatible with vanilla Neos. Furthermore, mods cannot alter the [data model] because NeosModLoader does not perform important component post-processing. 22 | 23 | In order to make a client using NeosModLoader compatible with vanilla Neos the compatibility hash must be restored to its default value. [NeosVersionReset.cs](../NeosModLoader/NeosVersionReset.cs) does the following: 24 | 25 | 1. Finds the original protocol version number by scanning FrooxEngine for a particular integer. This involves interpreting the IL bytes as instructions, which could be considered disassembling. 26 | 2. Calculates the default compatibility hash using the integer we just found. 27 | 3. Restores version and compatibility hash fields to their original values via [reflection](https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/reflection). This does not modify Neos code, but it does change the values of fields that are not intended to be changed. 28 | 29 | [plugin]: https://wiki.neos.com/Plugins 30 | [plugins]: https://wiki.neos.com/Plugins 31 | [component]: https://wiki.neos.com/Component 32 | [components]: https://wiki.neos.com/Component 33 | [logix]: https://wiki.neos.com/LogiX 34 | [data model]: https://wiki.neos.com/Core_Concepts#Data_Model 35 | 36 | ## Other Minor Ways NeosModLoader Interfaces with Neos 37 | 38 | - Hooking into the splash screen to show mod loading progress 39 | - Hooking into Neos shutdown to trigger one last mod configuration save 40 | -------------------------------------------------------------------------------- /doc/img/NeosProLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos-modding-group/NeosModLoader/e5a3e8a2042966e722902de61acc3d6f1118cf9a/doc/img/NeosProLauncher.png -------------------------------------------------------------------------------- /doc/img/NeosPublicSetup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos-modding-group/NeosModLoader/e5a3e8a2042966e722902de61acc3d6f1118cf9a/doc/img/NeosPublicSetup.png -------------------------------------------------------------------------------- /doc/img/add_non_steam_game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos-modding-group/NeosModLoader/e5a3e8a2042966e722902de61acc3d6f1118cf9a/doc/img/add_non_steam_game.png -------------------------------------------------------------------------------- /doc/img/non_steam_game_properties_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos-modding-group/NeosModLoader/e5a3e8a2042966e722902de61acc3d6f1118cf9a/doc/img/non_steam_game_properties_1.png -------------------------------------------------------------------------------- /doc/img/non_steam_game_properties_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos-modding-group/NeosModLoader/e5a3e8a2042966e722902de61acc3d6f1118cf9a/doc/img/non_steam_game_properties_2.png -------------------------------------------------------------------------------- /doc/img/steam_game_properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos-modding-group/NeosModLoader/e5a3e8a2042966e722902de61acc3d6f1118cf9a/doc/img/steam_game_properties.png -------------------------------------------------------------------------------- /doc/img/windows_unblock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neos-modding-group/NeosModLoader/e5a3e8a2042966e722902de61acc3d6f1118cf9a/doc/img/windows_unblock.png -------------------------------------------------------------------------------- /doc/linux.md: -------------------------------------------------------------------------------- 1 | # Linux Notes 2 | 3 | NeosModLoader works on Linux, but in addition to the [normal install steps](../README.md#installation) there are some extra steps you need to take until Neos issue [#2638](https://github.com/Neos-Metaverse/NeosPublic/issues/2638) is fixed. 4 | 5 | The log directory on Linux is `$HOME/.local/share/Steam/steamapps/common/NeosVR/Logs` 6 | 7 | If your log contains the following, you need to set up a workaround for the issue. 8 | 9 | ```log 10 | System.IO.DirectoryNotFoundException: Could not find a part of the path "/home/myusername/.local/share/Steam/steamapps/common/NeosVR/Neos_Data\Managed/FrooxEngine.dll". 11 | ``` 12 | 13 | To set up the workaround, run the following commands in your terminal: 14 | 15 | ```bash 16 | cd "$HOME/.local/share/Steam/steamapps/common/NeosVR" 17 | ln -s Neos_Data/Managed 'Neos_Data\Managed' 18 | ``` 19 | -------------------------------------------------------------------------------- /doc/making_mods.md: -------------------------------------------------------------------------------- 1 | # Mod Creation Guide 2 | 3 | If you have some level of familiarity with C#, getting started making mods should not be too difficult. 4 | 5 | ## Basic Visual Studio setup 6 | 7 | 1. Make a new .NET library against .NET version 4.6.2. You can use 4.7.2 if you absolutely need it in order to compile, but some features may not work. 8 | 2. Add NeosModLoader.dll as a reference. 9 | 3. Add references to Neos libraries as needed (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Neos_Data\Managed`) 10 | 4. Remove the reference to `System.Net.Http` as it will make the compiler angry 11 | 12 | ## Hooks 13 | 14 | ### `OnEngineInit()` 15 | 16 | Called once during FrooxEngine initialization. 17 | 18 | Happens **before** `OnEngineInit()` 19 | 20 | - Head device setup 21 | - Plugin initialization 22 | 23 | Happens **after** `OnEngineInit()` 24 | 25 | - Local DB initialization 26 | - Networking initialization 27 | - Audio initialization 28 | - Worlds loading, including Local home and Userspace 29 | 30 | ## Mod Configuration 31 | 32 | NeosModLoader provides a built-in configuration system that can be used to persist configuration values for mods. More information is available in the [configuration system documentation](config.md). 33 | 34 | ## Example Mod 35 | 36 | ```csharp 37 | using HarmonyLib; // HarmonyLib comes included with a NeosModLoader install 38 | using NeosModLoader; 39 | using System; 40 | using System.Reflection; 41 | 42 | namespace MyMod 43 | { 44 | public class MyMod : NeosMod 45 | { 46 | public override string Name => "MyMod"; 47 | public override string Author => "your name here"; 48 | public override string Version => "1.0.0"; 49 | public override string Link => "https://github.com/myName/myRepo"; // this line is optional and can be omitted 50 | 51 | private static bool _first_trigger = false; 52 | 53 | public override void OnEngineInit() 54 | { 55 | Harmony harmony = new Harmony("some string unique to MyMod"); 56 | // do whatever LibHarmony patching you need 57 | 58 | Debug("a debug log"); 59 | Msg("a regular log"); 60 | Warn("a warn log"); 61 | Error("an error log"); 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | A [Template repo](https://github.com/EIA485/NeosTemplate/) is available. 68 | 69 | ## Full Example 70 | 71 | A working example mod is available here: 72 | 73 | It showcases the following: 74 | 75 | - A valid Visual Studio project setup 76 | - Using LibHarmony to patch a Neos method 77 | - Using Unity to alter all existing GameObjects of a certain type 78 | 79 | ## Additional Resources 80 | 81 | - [Quick C# Refresher](https://learnxinyminutes.com/docs/csharp/) 82 | - [LibHarmony Documentation](https://harmony.pardeike.net/) 83 | - [Unity API Documentation](https://docs.unity3d.com/ScriptReference/index.html) 84 | - [Neos Plugin Wiki Page](https://wiki.neos.com/Plugins) 85 | -------------------------------------------------------------------------------- /doc/modloader_config.md: -------------------------------------------------------------------------------- 1 | # Modloader Configuration 2 | 3 | NeosModLoader aims to have a reasonable default configuration, but certain things can be adjusted via an optional config file. The config file does not create itself automatically, but you can create it yourself by making a `NeosModLoader.config` file in the same directory as `NeosModLoader.dll`. `NeosModLoader.config` is a simple text file that supports keys and values in the following format: 4 | 5 | ```ini 6 | debug=true 7 | nomods=false 8 | ``` 9 | 10 | Not all keys are required to be present. Missing keys will use the defaults outlined below: 11 | 12 | | Configuration | Default | Description | 13 | | ------------------ | ------- | ----------- | 14 | | `debug` | `false` | If `true`, NeosMod.Debug() logs will appear in your log file. Otherwise, they are hidden. | 15 | | `hidevisuals` | `false` | If `true`, NML won't show a loading indicator on the splash screen. | 16 | | `nomods` | `false` | If `true`, mods will not be loaded from `nml_mods`. | 17 | | `nolibraries` | `false` | If `true`, extra libraries from `nml_libs` will not be loaded. | 18 | | `advertiseversion` | `false` | If `false`, your version will be spoofed and will resemble `2021.8.29.1240`. If `true`, your version will be left unaltered and will resemble `2021.8.29.1240+NeosModLoader.dll`. This version string is visible to other players under certain circumstances. | 19 | | `unsafe` | `false` | If `true`, the version spoofing safety check is disabled and it will still work even if you have other Neos plugins. DO NOT load plugin components in multiplayer sessions, as it will break things and cause crashes. Plugin components should only be used in your local home or user space. | 20 | | `logconflicts` | `true` | If `false`, conflict logging will be disabled. If `true`, potential mod conflicts will be logged. If `debug` is also `true` this will be more verbose. | 21 | | `hidemodtypes` | `true` | If `true`, mod-related types will be hidden in-game. If `false`, no types will be hidden, which makes NML detectable in-game. | 22 | | `hidelatetypes` | `true` | If `true` and `hidemodtypes` is `true`, late loaded types will be hidden in-game. If `false`, late loaded types will be shown | -------------------------------------------------------------------------------- /doc/neos_guidelines.md: -------------------------------------------------------------------------------- 1 | # Modding related guidelines 2 | 3 | ## Is Modding Neos Allowed? 4 | 5 | Yes! 6 | 7 | On January 11, 2022, an official [Mod & Plugin Policy] was released. Mods are now officially allowed, so long as the policy is followed. 8 | 9 | ## Is Modding Good for Neos? 10 | 11 | > Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should. 12 | > — Dr. Ian Malcolm 13 | 14 | My opinion is yes, mods are healthy for the game. You might have deduced this from the fact that I created a mod loader. But let's look at the pros and cons, and I'll try to be impartial. 15 | 16 | ### The Pros 17 | 18 | - Tomorrow's quality of life improvements, today. Sometimes the Neos team needs to direct their efforts elsewhere. Allowing the community to make temporary fixes pending a permanent solution is an easy win. Example: [MotionBlurDisable](https://github.com/zkxs/MotionBlurDisable), which won't be implemented in Neos until a full Settings redesign. 19 | - Mods let power users opt in to warranty-voiding tools. Sometimes a desired feature has a very niche audience and risks breaking in the future. But at the same time it can be very useful in the present. Example: [ExportNeosToJson](https://github.com/zkxs/ExportNeosToJson). 20 | - The classic "if you outlaw guns only outlaws will have guns" argument. Example: \[redacted\]. 21 | 22 | 23 | 24 | ### The Cons 25 | 26 | - Risk of abuse. The more control users have over the game the more damage they can potentially do. 27 | - Risk of breakage. Users *should* be aware that mods void the warranty, but some people might complain in the wrong channels when mods break. And if things go very wrong there's potential for a mod to break an unmodified user over the network. 28 | - Misdirection of effort. If mods create a problem the Neos team has to step in and solve that's less time spent improving the game. 29 | 30 | 31 | ## Afterword 32 | 33 | The [EULA], [guidelines] and [Mod & Plugin Policy] are subject to change. This document was last updated on **2022-01-12**, and may be out of date. 34 | 35 | [eula]: https://store.steampowered.com/eula/740250_eula_0 36 | [guidelines]: https://docs.google.com/document/d/1mqdbIvbj1b2LeFhNzfAASeTpRZk6vmbXISYLdTXTVR4/edit 37 | [Mod & Plugin Policy]: https://wiki.neos.com/Mod_%26_Plugin_Policy 38 | [privacy policy]: https://wiki.neos.com/Neos_Wiki:Privacy_policy 39 | -------------------------------------------------------------------------------- /doc/neos_standalone_setup.md: -------------------------------------------------------------------------------- 1 | # Neos Standalone Setup 2 | 3 | How to have steam hour logging, mods, and NCR simultaneously 4 | 5 | ## Explanation 6 | 7 | If you don't care about why we're doing things and just want the steps, skip to the [setup](#setup) section. 8 | 9 | First, lets about how Neos is shipped. Both the Steam and standalone versions come with a launcher. This is called NeosLauncher.exe. This is *only* a helper program that launches Neos with extra [command line arguments](https://wiki.neos.com/Command_Line_Arguments). You do not need to launch Neos via NeosLauncher.exe if you have a different way of passing in arguments, for example via Steam's launch options. Next, Neos standalone also comes with a special NeosProLauncher.exe. This application updates the standalone install when opened and can launch Neos, but notably **it cannot pass command line arguments**. This means while we need it for updating, we cannot launch modded Neos using it. 10 | 11 | Finally, lets talk Steam integration. As long as Steam is running *before* Neos starts, Neos will hook up with Steam. This enables Steam playtime logging and Steam networking. You do not actually need to launch via Steam, however launching via Steam will guarantee you don't forget to open your Steam client. 12 | 13 | So to summarize: 14 | 15 | Neos.exe is the actual game. We need to pass special command line arguments to this. There are three commonly used launchers: 16 | 17 | | Launcher | Can update Neos? | Can pass command line arguments? | 18 | | ------------------- | ----------------------- | -------------------------------- | 19 | | NeosLauncher.exe | No | Yes | 20 | | Steam | Steam version only | Yes | 21 | | NeosProLauncher.exe | Standalone version only | No | 22 | 23 | Therefore, my suggestion is you use NeosProLauncher to keep your install updated and a non-Steam game shortcut within steam to actually launch Neos. 24 | 25 | ## Setup 26 | 27 | 1. Grab and run [NeosPublicSetup.exe](https://assets.neos.com/install/NeosPublicSetup.exe) 28 | 2. Install it wherever you want to. The default `C:\Neos` location is fine if you don't have multiple drives to worry about. If you do, check the [drive notes](directories.md/#drive-notes). **Do not merge your standalone install into your Steam install!** While merging installs can technically work it can easily create more problems than it solves. 29 | ![NeosPublicSetup.exe screenshot](img/NeosPublicSetup.png) 30 | 3. Go to the directory you installed to and run `NeosProLauncher.exe`. Wait for it to finish patching. **Do not use either of the launch buttons!** We're only using this to update Neos, not launch it. 31 | ![NeosProLauncher.exe screenshot](img/NeosProLauncher.png) 32 | 4. Once the pro launcher says it's ready, simply exit it without launching. 33 | 5. Observe that an `app` directory has been created next to `NeosProLauncher.exe`. So, for example, it may be in `C:\Neos\app`. This `app` directory contains the standalone neos install, and contains the `NeosLauncher.exe` and `Neos.exe` that you are familiar with from the Steam install. 34 | 6. Go to steam and add a non-steam game. 35 | ![add non-steam game screenshot](img/add_non_steam_game.png) 36 | 7. Hit the "browse" button, and go to `C:\Neos\app\Neos.exe` (or wherever is applicable given your install directory) 37 | 8. Hit the "add selected programs" button. 38 | 9. Right click the newly added game in your library, and go to "properties" 39 | ![right click properties screenshot](img/non_steam_game_properties_1.png) 40 | 10. Configure the non-steam game with the same launch options you would use on the Steam version (`-LoadAssembly Libraries\NeosModLoader.dll`). Optionally, give it a more descriptive name and check the "Include in VR Library" checkbox. 41 | ![non steam game properties screenshot](img/non_steam_game_properties_2.png) 42 | 11. Install NeosModLoader into the standalone version [as you normally would for the steam version](../README.md#installation), but using your `C:\Neos\app` directory instead. 43 | 12. Launch Neos using your new non-steam game shortcut. Steam will track playtime on the Steam version of the game even though you are running the standalone version. 44 | 45 | ## Important Notes 46 | 47 | - You will need to run `NeosProLauncher.exe` every time you want to update. You never need actually use its launch buttons, as it does not support launch options which are required for plugins/mods. 48 | - The logs you're used to finding in `C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Logs` will go to `C:\Neos\app\Logs` now. 49 | -------------------------------------------------------------------------------- /doc/problem_solving_techniques.md: -------------------------------------------------------------------------------- 1 | # Problem Solving Techniques 2 | 3 | Neos has many different ways to solve a problem: Components, Logix, HTTP connections to an external server, refhacking, plugins, and now mods. Some of these methods are supported, some aren't. We'll do a quick rundown of the methods and where they're applicable. 4 | 5 | ## Components and Logix 6 | 7 | If you can solve a problem with Components and/or Logix that's probably the approach you should take, as the more advanced techniques are likely to be overkill. 8 | 9 | ## HTTP Connections 10 | 11 | Neos provides Logix nodes to communicate with an external server via HTTP GET, POST, and websockets. This is great for: 12 | 13 | - Heavy data processing that LogiX isn't well suited for 14 | - Advanced data persistence (for simple things consider using Cloud Variables) 15 | - Connecting Logix across multiple sessions 16 | 17 | ## RefHacking 18 | 19 | Refhacking is a method that at considerable performance cost can get you an extremely sketchy but working component access. Refhacking is not supported and will break in the future, but sometimes it is the only way to do certain things without waiting for real component access. 20 | 21 | My personal advice is to just put your component access ideas into a todo list and do them once we have real support. It's not fun when your creations break. 22 | 23 | ## Plugins 24 | 25 | Plugins let you add new components and Logix nodes, but at the cost of breaking multiplayer compatibility. If you like multiplayer, they aren't going to give you much quality-of-life because you'll be forced into singleplayer to use them. Plugins are great for automating menial tasks that you do very infrequently, for example monopacking is nightmarish to do by hand but can be done with a single button via a plugin. 26 | 27 | ## Mods 28 | 29 | Mods do **not** let you add new components and Logix nodes, but they do work in multiplayer. They are limited in what they can do without breaking multiplayer compatibility. You can imagine them as a "controlled desync". They are well-suited for minor quality-of-life tweaks, for example preventing your client from rendering motion blur. Making a larger feature with a mod isn't a great option, as you cannot rely on other clients also having the mod. 30 | -------------------------------------------------------------------------------- /doc/start_neos.bat: -------------------------------------------------------------------------------- 1 | @start Neos.exe -LoadAssembly "Libraries\NeosModLoader.dll" 2 | -------------------------------------------------------------------------------- /doc/troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting NeosModLoader 2 | 3 | Below we will go over some common problems and their solutions. 4 | 5 | ## NeosModLoader Isn't Being Loaded 6 | 7 | **Symptoms:** 8 | 9 | - After starting the game nothing has changed, and it appears completely unmodified. 10 | - Logs don't say anything about "NeosModLoader" 11 | 12 | **Fix:** 13 | 14 | If the problem is the `-LoadAssembly` setup: 15 | 16 | 1. Check the logs (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Logs`). If you search the log for "NeosModLoader" you should find a section that looks like this: 17 | 18 | ```log 19 | 5:26:23 PM.305 ( 0 FPS) Argument: Neos.exe 20 | 5:26:23 PM.305 ( 0 FPS) Argument: -LoadAssembly 21 | 5:26:23 PM.305 ( 0 FPS) Argument: Libraries\NeosModLoader.dll 22 | ``` 23 | 24 | If those logs are absent it indicates you are not passing the `-LoadAssembly Libraries\NeosModLoader.dll` argument to Neos correctly. 25 | 2. Double check your shortcut to Neos. 26 | 3. Check a known-working shortcut. 27 | 1. Navigate to the Neos install directory. (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR`) 28 | 2. Create a new text file named `start_neos.bat` in your Neos install directory. Make sure the file extension is `.bat` and not`.txt`. 29 | 3. Copy the contents of the example [start_neos.bat](start_neos.bat) into yours. 30 | 4. Run your `start_neos.bat` by double-clicking it in your file explorer. 31 | 5. Neos should start and load NeosModLoader as expected. 32 | 33 | If the problem is the FrooxEngine.dll path on Linux: 34 | 35 | 1. If you are on Linux, make sure you've followed the [extra Linux instructions](linux.md). 36 | 37 | If the problem is Windows blocking the DLL file: 38 | 39 | 1. Right click on the NeosModLoader.dll file and open the properties. 40 | 2. Check the unblock checkbox, and hit OK. 41 | ![add non-steam game screenshot](img/windows_unblock.png) 42 | 3. Repeat this process for 0Harmony.dll. 43 | 44 | If the problem is your antivirus: 45 | 46 | 1. Make sure your antivirus has not quarantined or deleted NeosModLoader.dll or 0Harmony.dll. 47 | 2. Add an exception to your antivirus. If you're uncomfortable adding an exception, you have options: 48 | - Don't run NeosModLoader. 49 | - Change to an antivirus that has fewer false positives. 50 | - Build NeosModLoader and/or Harmony yourself from source code. 51 | 52 | ## NeosModLoader Loads, but Errors Out 53 | 54 | **Symptoms:** 55 | 56 | - Mods are not loading 57 | - All of your contacts have a magenta border and appear to be using an incompatible version 58 | 59 | **Fix:** 60 | 61 | 1. Verify that the [installation instructions](../README.md#installation) were followed correctly 62 | 2. If you are using the [standalone](neos_standalone_setup.md) or [Linux](linux.md) builds, make sure you've followed the extra steps. 63 | 3. Check the logs (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Logs`). There are a few things you are likely to find: 64 | 65 | Possibility 1: Harmony is not installed correctly. 66 | 67 | 1. Your log contains the following: 68 | 69 | ```log 70 | 18:36:34.158 ( 0 FPS) [ERROR][NeosModLoader] Exception in execution hook! 71 | System.IO.FileNotFoundException: Could not load file or assembly '0Harmony, Version=2. 2.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. 72 | File name: '0Harmony, Version=2.2.0.0, Culture=neutral, PublicKeyToken=null' 73 | at NeosModLoader.ExecutionHook..cctor () [0x00000] in <67d6a64d7ebf403f83f1a8b1d8c03d22>:0 74 | ``` 75 | 76 | 2. Go back to the [installation instructions](../README.md#installation) and install Harmony to the correct location. 77 | 78 | Possibility 2: You are using an old version of NeosModLoader. 79 | 80 | 1. Check your log for a line like this: 81 | 82 | ```log 83 | 5:26:24 PM.823 ( 0 FPS) [INFO] [NeosModLoader] NeosModLoader v1.8.0 starting up! 84 | ``` 85 | 86 | 2. Verify your NeosModLoader version matches [the latest release](https://github.com/neos-modding-group/NeosModLoader/releases/latest). 87 | 88 | Possibility 3: NeosModLoader itself is broken, even on the latest version. This can happen in rare circumstances when Neos updates. 89 | 90 | 1. Please report the issue on [our Discord][Neos Modding Discord] or in [a GitHub issue](https://github.com/neos-modding-group/NeosModLoader/issues). 91 | 2. Wait for a fix. 92 | 93 | ## Multiplayer Compatibility is Broken, but Everything Else Works 94 | 95 | **Symptoms:** 96 | 97 | - Mods are loading 98 | - All of your contacts have a magenta border and appear to be using an incompatible version 99 | 100 | **Fix:** 101 | 102 | 1. Make sure you are not running more than one plugin. For safety reasons, NeosModLoader will only spoof your version if it is the only plugin running. 103 | 2. If you absolutely need your other plugin and understand the risks there is a [configuration](modloader_config.md) available to force version spoofing. 104 | 105 | ## A Mod is Breaking Neos 106 | 107 | **Symptoms:** 108 | 109 | - Modded Neos is broken or crashing unexpectedly 110 | - Unmodified Neos is working 111 | 112 | **Fix:** 113 | 114 | Remove the offending mod, and contact its developer so they can fix the bug. 115 | 116 | If you are not sure which mod is broken, follow the below steps: 117 | 118 | 1. Check the logs (`C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Logs`). They should indicate which mod is failing. If the logs don't help, then continue with the following steps. 119 | 2. Disable NeosModLoader by removing the `-LoadAssembly Libraries\NeosModLoader.dll` launch option. If Neos is still having problems while completely unmodified, you can get support on the [Neos Discord](https://discordapp.com/invite/GQ92NUu5). **You should not ask the Neos Discord for help with mods.** 120 | 3. If you only experience the problem while modded, try uninstalling all of your mods and re-installing them one by one. Once you find the problematic mod reach out it its developers. 121 | 4. If the issue appears to be with NeosModLoader itself, please open [an issue](https://github.com/neos-modding-group/NeosModLoader/issues). 122 | 123 | ## I Need More Help 124 | 125 | If you are having trouble diagnosing the issue yourself, we have a #help-and-support channel in the [Neos Modding Discord]. The first thing we're likely to ask for is your log, so please have that handy. You can find logs here: `C:\Program Files (x86)\Steam\steamapps\common\NeosVR\Logs` 126 | 127 | 128 | [Neos Modding Discord]: https://discord.gg/vCDJK9xyvm 129 | --------------------------------------------------------------------------------