thunderstore prep, got rope storage working

This commit is contained in:
Sakimori 2025-04-17 22:55:24 -04:00
parent ae461e46df
commit c3ec0dfa0a
16 changed files with 403 additions and 45 deletions

7
.editorconfig Normal file
View file

@ -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

55
.github/workflows/build.yml vendored Normal file
View file

@ -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

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/.idea
/.vs
/thunderstore/build
obj
bin

25
ExampleMod.sln Normal file
View file

@ -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

View file

@ -1,28 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>GlitchRestore</AssemblyName>
<Product>My first plugin</Product>
<Version>1.0.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<RestoreAdditionalProjectSources>
https://api.nuget.org/v3/index.json;
https://nuget.bepinex.dev/v3/index.json;
https://nuget.samboy.dev/v3/index.json
</RestoreAdditionalProjectSources>
<RootNamespace>GlitchRestore</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all" />
<PackageReference Include="BepInEx.Core" Version="5.*" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.*" />
<PackageReference Include="UnityEngine.Modules" Version="2022.3.33" IncludeAssets="compile" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all" />
</ItemGroup>
</Project>

View file

@ -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!");
}
}

21
LICENSE.md Normal file
View file

@ -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.

6
NuGet.Config Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="BepInEx" value="https://nuget.bepinex.dev/v3/index.json" />
</packageSources>
</configuration>

59
README.md Normal file
View file

@ -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 `<AssemblyName>` to your mod name
2. Make sure the `<NineSolsPath>` 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:
<img alt="r2modman config to import local mod" src="https://github.com/user-attachments/assets/c8e02c83-5d71-4a65-89ef-acf93db85327" width="600">
## 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 `<ProfileDir>` and uncomment the `<CopyDir>` 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.

81
Source/ExampleMod.cs Normal file
View file

@ -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<bool> enableSomethingConfig = null!;
private ConfigEntry<KeyboardShortcut> 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<Player, bool>
PlayerHasHat = AccessTools.FieldRefAccess<Player, bool>("_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();
}
}

View file

@ -0,0 +1,70 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>GlitchRestore</AssemblyName>
<Description>Patch speedrun glitches from the speedrun branch back into the main branch game.</Description>
<TargetFramework>netstandard2.1</TargetFramework>
<Version>1.0.0</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<PublishRelease>true</PublishRelease>
<NoWarn>MSB3277</NoWarn>
<!-- todo macOS -->
<NineSolsPath Condition="'$(OS)' == 'Windows_NT'">E:\SteamLibrary\steamapps\common\Nine Sols</NineSolsPath>
<NineSolsPath Condition="'$(OS)' != 'Windows_NT'">$(HOME)/.local/share/Steam/steamapps/common/Nine Sols</NineSolsPath>
<DllPath>$(NineSolsPath)/NineSols_Data/Managed</DllPath>
<!-- If you're not using R2Modman/Thunderstore, this can be NineSolsPath as well. Only used in CopyDir -->
<ProfileDir Condition="'$(OS)' == 'Windows_NT'">E:\SteamLibrary\steamapps\common\Nine Sols</ProfileDir>
<ProfileDir Condition="'$(OS)' != 'Windows_NT'">$(HOME)/.config/r2modmanPlus-local/NineSols/profiles/Default</ProfileDir>
<!-- After building, copy the dll to this folder. Useful for hot-reloading: https://github.com/BepInEx/BepInEx.Debug/blob/master/README.md#scriptengine -->
<CopyDir>$(ProfileDir)/BepInEx/scripts</CopyDir>
</PropertyGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp">
<HintPath>$(DllPath)/Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="rcg.rcgmakercore.Runtime">
<HintPath>$(DllPath)/rcg.rcgmakercore.Runtime.dll</HintPath>
</Reference>
<Reference Include="RCG_General">
<HintPath>$(DllPath)/RCG_General.dll</HintPath>
</Reference>
<Reference Include="InControl">
<HintPath>$(DllPath)/InControl.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="BepInEx.Analyzers" Version="1.*" PrivateAssets="all" />
<PackageReference Include="BepInEx.Core" Version="5.*" />
<PackageReference Include="BepInEx.PluginInfoProps" Version="2.*" />
<PackageReference Include="UnityEngine.Modules" Version="2022.3.18" IncludeAssets="compile" />
<!-- enable below if you want to use the API -->
<PackageReference Include="NineSolsAPI" Version="1.2.1" />
<!-- or locally <ProjectReference Include="../../NineSolsAPI/NineSolsAPI/NineSolsAPI.csproj" />-->
</ItemGroup>
<Target Name="CheckReferences" BeforeTargets="BeforeBuild">
<ItemGroup>
<MissingReferences Include="@(Reference)" Condition="!Exists('%(Reference.HintPath)')" />
</ItemGroup>
<Error Condition="@(MissingReferences->Count()) > 0" Text="Missing reference(s);
@(MissingReferences->'%(HintPath)', ',&#x0A;')
Did you forget to adjust your NineSolsPath '$(NineSolsPath)'?" />
</Target>
<Target Name="CopyMod" AfterTargets="PostBuildEvent" Condition="'$(CopyDir)' != ''">
<Message Importance="high" Text="copying $(TargetPath) to $(CopyDir) ..." />
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(CopyDir)" SkipUnchangedFiles="true" />
<Copy SourceFiles="$(TargetDir)$(TargetName).pdb" DestinationFolder="$(CopyDir)" SkipUnchangedFiles="true" />
</Target>
<Target Name="PackageMod" AfterTargets="Publish">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="../thunderstore/build/dll" SkipUnchangedFiles="true" />
<Exec Command="tcli build --config-path ../thunderstore/thunderstore.toml" />
</Target>
</Project>

23
Source/Log.cs Normal file
View file

@ -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);
}

19
Source/Patches.cs Normal file
View file

@ -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
}
}

4
thunderstore/README.md Normal file
View file

@ -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.

BIN
thunderstore/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -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" ]