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: +r2modman config to import local mod + + +## 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" ]