diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..bdfffd9
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*.cs]
+csharp_new_line_before_open_brace = none
+csharp_new_line_before_else = false
+csharp_new_line_before_catch = false
+csharp_new_line_before_finally = false
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..94a92e0
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,55 @@
+name: build
+
+on:
+ push:
+ branches: [ "main" ]
+ tags:
+ - 'v*'
+ pull_request:
+ branches: [ "main" ]
+ workflow_dispatch:
+
+jobs:
+ build:
+ permissions:
+ contents: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Download libs-stripped
+ uses: actions/checkout@v3
+ with:
+ repository: nine-sols-modding/libs-stripped
+ path: libs-stripped
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+ - name: Restore dependencies
+ run: dotnet restore
+ - name: Install tcli
+ run: dotnet tool install -g tcli
+ - name: Check against all game versions
+ run: |
+ cut -f1 -d' ' libs-stripped/versions.txt | while IFS= read -r version; do
+ echo "Checking $version"
+ dotnet build --no-restore -p:DllPath="$PWD/libs-stripped/$version"
+ done
+ - name: Publish build
+ run: |
+ publish_version=$(cut -f1 -d' ' libs-stripped/versions.txt | tail -n1)
+ dotnet publish --no-restore -p:DllPath="$PWD/libs-stripped/$publish_version"
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: build
+ path: |
+ thunderstore/build/*.zip
+ thunderstore/build/dll/*.dll
+ - name: Release
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ thunderstore/build/*.zip
+ thunderstore/build/dll/*.dll
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6d0bce9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+/.idea
+/.vs
+
+/thunderstore/build
+obj
+bin
diff --git a/ExampleMod.sln b/ExampleMod.sln
new file mode 100644
index 0000000..c14ba38
--- /dev/null
+++ b/ExampleMod.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.34928.147
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlitchRestore", "Source\GlitchRestore.csproj", "{FC7FF0C4-8F9F-43B9-963F-5C8B3B8920AF}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {FC7FF0C4-8F9F-43B9-963F-5C8B3B8920AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC7FF0C4-8F9F-43B9-963F-5C8B3B8920AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC7FF0C4-8F9F-43B9-963F-5C8B3B8920AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC7FF0C4-8F9F-43B9-963F-5C8B3B8920AF}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {2357817E-B903-4DD8-B0FD-64E75AAFD672}
+ EndGlobalSection
+EndGlobal
diff --git a/GlitchRestore/GlitchRestore.csproj b/GlitchRestore/GlitchRestore.csproj
deleted file mode 100644
index 04a5417..0000000
--- a/GlitchRestore/GlitchRestore.csproj
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- netstandard2.0
- GlitchRestore
- My first plugin
- 1.0.0
- true
- latest
-
- https://api.nuget.org/v3/index.json;
- https://nuget.bepinex.dev/v3/index.json;
- https://nuget.samboy.dev/v3/index.json
-
- GlitchRestore
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/GlitchRestore/Plugin.cs b/GlitchRestore/Plugin.cs
deleted file mode 100644
index 648dbb6..0000000
--- a/GlitchRestore/Plugin.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using BepInEx;
-using BepInEx.Logging;
-
-namespace GlitchRestore;
-
-[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
-public class Plugin : BaseUnityPlugin
-{
- internal static new ManualLogSource Logger;
-
- private void Awake()
- {
- // Plugin startup logic
- Logger = base.Logger;
- Logger.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!");
- }
-}
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..c13f991
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..1864ded
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9a1020d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,59 @@
+# Nine Sols Example Mod
+
+## Set up your mod
+1. Set up modding with BepInEx and r2modman: [Wiki: Getting Started](https://github.com/nine-sols-modding/Resources/wiki/Getting-started)
+ 1. download the `NineSolsAPI` mod if you want to use it,
+2. clone this repo ([generate from this template](https://github.com/new?template_name=NineSols-ExampleMod&template_owner=jakobhellermann), then update the `.csproj`
+ 1. Change `` to your mod name
+ 2. Make sure the `` points to the installed game
+3. Install `tcli` tool for building thunderstore mods: `dotnet tool install -g tcli`
+4. Follow the **Building** section to make sure everything works as expected. Load into a game and press `Ctrl-H` to toggle your hat wherever you are.
+
+_Next steps_:
+- setup hot reloading for faster iteration times
+- use a tool like [ILSpy](https://github.com/icsharpcode/ILSpy) or [dnSpy](https://github.com/dnSpy/dnSpy) to decompile the game code
+- check out the [UnityExplorer](https://thunderstore.io/c/nine-sols/p/ninesolsmodding/UnityExplorer/) mod to investigate objects in the game
+
+## Building
+
+If you run
+```sh
+dotnet publish
+```
+it will build the DLL of your mod (`Source/bin/Release/netstandard2.1/publish/ExampleMod.dll`), then use `tcli` to
+package the mod into a thunderstore-compatible zip in `thunderstore/build/`.
+
+You can import that mod into your r2modman instance like this:
+
+
+
+## Publishing
+
+Make sure to fill out all fields in the [thunderstore.toml](./thunderstore/thunderstore.toml).
+Then go to https://thunderstore.io/c/nine-sols/create and upload your mod zip, or use tcli with a token created in
+[thunderstore.io](thunderstore.io) at `Settings / Teams / Service Accounts`:
+```sh
+tcli build --config-path ../thunderstore/thunderstore.toml --token $token
+```
+
+## Hot Reloading
+
+Building the mod and restarting the game after every minor change becomes cumbersome quickly. Luckily, BepInEx supports hot reloading of DLLs via [ScriptEngine](https://github.com/BepInEx/BepInEx.Debug).
+
+Download the [ScriptEngine](https://thunderstore.io/c/nine-sols/p/ninesolsmodding/BepinExScriptEngine/) mod in your r2modman instance, and the game will be able to reload DLLs from `r2modmanProfileFolder/BepInEx/scripts/`.
+
+Go into the `ExampleMod.csproj` file and fill out the `` and uncomment the `` below it.
+Now, whenever you hit "Build" in your IDE, the mod DLL will be placed into that `scripts` folder.
+
+Note: **Disable your mod in r2modman if it is active to prevent it from being loaded twice!**
+
+By default, ScriptEngine will only reload scripts when you press `F6`, but you can go into r2modman's Config Editor and
+edit `BepInEx\config\com.bepis.bepinex.scriptengine.cfg` to have
+- `EnableFileSystemWatcher=true`
+- `AutoReloadDelay=0`
+- `LoadOnStart=true`
+
+to reload scripts immediately.
+
+Hot reloading works by first destroying your mod instance game objects and then reinstantiating them, so make sure to clean
+up any state you left in the `OnDestroy` callback.
\ No newline at end of file
diff --git a/Source/ExampleMod.cs b/Source/ExampleMod.cs
new file mode 100644
index 0000000..a2a3c64
--- /dev/null
+++ b/Source/ExampleMod.cs
@@ -0,0 +1,81 @@
+using BepInEx;
+using BepInEx.Configuration;
+using HarmonyLib;
+using NineSolsAPI;
+using System;
+using UnityEngine;
+
+namespace GlitchRestore;
+
+[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
+public class ExampleMod : BaseUnityPlugin {
+ // https://docs.bepinex.dev/articles/dev_guide/plugin_tutorial/4_configuration.html
+ private ConfigEntry enableSomethingConfig = null!;
+ private ConfigEntry somethingKeyboardShortcut = null!;
+
+ private Harmony harmony = null!;
+
+ [HarmonyPatch(typeof(PlayerHurtState), nameof(PlayerHurtState.OnStateEnter))]
+ internal static class RopeRestore {
+ private static void Prefix(Player ___player, out ClimbableRope __state) {
+ __state = ___player.touchingRope;
+ }
+
+ private static void Postfix(ClimbableRope __state, ref Player ___player) {
+ if (__state != null) {
+ ___player.touchingRope = __state;
+ }
+ }
+ }
+
+ //[HarmonyPatch(typeof(EffectDealer), nameof(EffectDealer.HitEffectSensorExitCheck))]
+ //internal static class DamageRespawnRestore {
+ //I think it's in EffectDealer but I'm not sure
+ //}
+
+ private void Awake() {
+ Log.Init(Logger);
+ RCGLifeCycle.DontDestroyForever(gameObject);
+
+ // Load patches from any class annotated with @HarmonyPatch
+ harmony = Harmony.CreateAndPatchAll(typeof(ExampleMod).Assembly);
+
+ enableSomethingConfig = Config.Bind("General.Something", "Enable", true, "Enable the thing");
+ somethingKeyboardShortcut = Config.Bind("General.Something", "Shortcut",
+ new KeyboardShortcut(KeyCode.H, KeyCode.LeftControl), "Shortcut to execute");
+
+ // Usage of the modding API is entirely optional.
+ // It provides utilities like the KeybindManager, utilities for Instantiating objects including the
+ // NineSols lifecycle hooks, displaying toast messages and preloading objects from other scenes.
+ // If you do use the API make sure do have it installed when running your mod, and keep the dependency in the
+ // thunderstore.toml.
+
+ KeybindManager.Add(this, TestMethod, () => somethingKeyboardShortcut.Value);
+
+ Logger.LogInfo($"Plugin {MyPluginInfo.PLUGIN_GUID} is loaded!");
+ }
+
+ // Some fields are private and need to be accessed via reflection.
+ // You can do this with `typeof(Player).GetField("_hasHat", BindingFlags.Instance|BindingFlags.NonPublic).GetValue(Player.i)`
+ // or using harmony access tools:
+ private static readonly AccessTools.FieldRef
+ PlayerHasHat = AccessTools.FieldRefAccess("_hasHat");
+
+ private void TestMethod() {
+ if (!enableSomethingConfig.Value) return;
+ ToastManager.Toast("Shortcut activated");
+ Log.Info("Log messages will only show up in the logging console and LogOutput.txt");
+
+ // Sometimes variables aren't set in the title screen. Make sure to check for null to prevent crashes.
+ if (Player.i == null) return;
+
+ var hasHat = PlayerHasHat.Invoke(Player.i);
+ Player.i.SetHasHat(!hasHat);
+ }
+
+ private void OnDestroy() {
+ // Make sure to clean up resources here to support hot reloading
+
+ harmony.UnpatchSelf();
+ }
+}
\ No newline at end of file
diff --git a/Source/GlitchRestore.csproj b/Source/GlitchRestore.csproj
new file mode 100644
index 0000000..acd7dfb
--- /dev/null
+++ b/Source/GlitchRestore.csproj
@@ -0,0 +1,70 @@
+
+
+ GlitchRestore
+ Patch speedrun glitches from the speedrun branch back into the main branch game.
+ netstandard2.1
+ 1.0.0
+ latest
+ enable
+ true
+ MSB3277
+
+
+
+ E:\SteamLibrary\steamapps\common\Nine Sols
+ $(HOME)/.local/share/Steam/steamapps/common/Nine Sols
+ $(NineSolsPath)/NineSols_Data/Managed
+
+
+ E:\SteamLibrary\steamapps\common\Nine Sols
+ $(HOME)/.config/r2modmanPlus-local/NineSols/profiles/Default
+
+ $(ProfileDir)/BepInEx/scripts
+
+
+
+
+ $(DllPath)/Assembly-CSharp.dll
+
+
+ $(DllPath)/rcg.rcgmakercore.Runtime.dll
+
+
+ $(DllPath)/RCG_General.dll
+
+
+ $(DllPath)/InControl.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/Log.cs b/Source/Log.cs
new file mode 100644
index 0000000..afbfd42
--- /dev/null
+++ b/Source/Log.cs
@@ -0,0 +1,23 @@
+using BepInEx.Logging;
+
+namespace GlitchRestore;
+
+internal static class Log {
+ private static ManualLogSource? logSource;
+
+ internal static void Init(ManualLogSource logSource) {
+ Log.logSource = logSource;
+ }
+
+ internal static void Debug(object data) => logSource?.LogDebug(data);
+
+ internal static void Error(object data) => logSource?.LogError(data);
+
+ internal static void Fatal(object data) => logSource?.LogFatal(data);
+
+ internal static void Info(object data) => logSource?.LogInfo(data);
+
+ internal static void Message(object data) => logSource?.LogMessage(data);
+
+ internal static void Warning(object data) => logSource?.LogWarning(data);
+}
\ No newline at end of file
diff --git a/Source/Patches.cs b/Source/Patches.cs
new file mode 100644
index 0000000..2284fc2
--- /dev/null
+++ b/Source/Patches.cs
@@ -0,0 +1,19 @@
+using HarmonyLib;
+
+namespace ExampleMod;
+
+[HarmonyPatch]
+public class Patches {
+
+ // Patches are powerful. They can hook into other methods, prevent them from runnning,
+ // change parameters and inject custom code.
+ // Make sure to use them only when necessary and keep compatibility with other mods in mind.
+ // Documentation on how to patch can be found in the harmony docs: https://harmony.pardeike.net/articles/patching.html
+ [HarmonyPatch(typeof(Player), nameof(Player.SetStoryWalk))]
+ [HarmonyPrefix]
+ private static bool PatchStoryWalk(ref float walkModifier) {
+ walkModifier = 1.0f;
+
+ return true; // the original method should be executed
+ }
+}
\ No newline at end of file
diff --git a/thunderstore/README.md b/thunderstore/README.md
new file mode 100644
index 0000000..ad947c6
--- /dev/null
+++ b/thunderstore/README.md
@@ -0,0 +1,4 @@
+# Nine Sols Example Mod
+
+Write a description of your mod here, it will be displayed on thunderstore.
+Remember to update the icon.png to something representing your mod.
\ No newline at end of file
diff --git a/thunderstore/icon.png b/thunderstore/icon.png
new file mode 100644
index 0000000..63cb499
Binary files /dev/null and b/thunderstore/icon.png differ
diff --git a/thunderstore/thunderstore.toml b/thunderstore/thunderstore.toml
new file mode 100644
index 0000000..8e532f6
--- /dev/null
+++ b/thunderstore/thunderstore.toml
@@ -0,0 +1,27 @@
+[config]
+schemaVersion = "0.0.1"
+
+[package]
+namespace = "yournamespace"
+name = "YourModName"
+versionNumber = "0.1.0"
+description = "your mod description"
+websiteUrl = "link to the git repo"
+containsNsfwContent = false
+
+[package.dependencies]
+"BepInEx-BepInExPack" = "5.4.2100"
+"ninesolsmodding-NineSolsAPI" = "0.0.2"
+
+[build]
+icon = "./icon.png"
+readme = "./README.md"
+outdir = "./build"
+
+[[build.copy]]
+source = "build/dll"
+target = "."
+
+[publish]
+repository = "https://thunderstore.io"
+communities = [ "nine-sols" ]