This is a BepInEx plugin for Shadow Gambit that wraps core game functionality and makes it easier to access for other plugins.
- Setup your BepInEx plugin project as you normally would.
- Download the latest Pirate Base Plugin release or compile it yourself.
- Place
PirateBasePlugin.dll
into your plugins source folder and add it as a dependency in theproject_name.csproj
file.
<Project Sdk="Microsoft.NET.Sdk">
...
<ItemGroup>
<Reference Include="PirateBasePlugin">
<HintPath>path\to\PirateBasePlugin.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>
- Add it as a dependency of your plugin class by adding the
BepInDependency
attribute:
//...
[BepInDependency(PirateBase.Plugin.c_pluginGUID)]
public class YourPlugin : BasePlugin
{
//...
}
Shadow Gambit uses IL2CPP, which unfortunately makes modding fairly complicated. Here are some things to watch out for:
- Your cannot cast Il2CPP objects normally. Instead you must use the
x.Cast<T>()
method. - If you directly override a IL2CPP method in a subclass, then you cannot call
base.method()
from within that method, as that would cause an infinite recursion. - If you start a thread you must "attach" it by calling either
IL2CPP.il2cpp_thread_attach(IL2CPP.il2cpp_domain_get())
orModThreadUtility.AttachThread()
from within the thread. - Some IL2CPP structs have been converted into classes. Be careful not to initialize these with
default
.- This can be annoying as some game methods initialize affected struct parameters with
default
. You must set these to something else when calling these methods or the game may crash.CharacterUtility
contains helper methods for some affected game methods.
- This can be annoying as some game methods initialize affected struct parameters with
- Some IL2CPP classes are not converted correctly and cannot be accessed directly from C#, e.g.
BalancedEnum
. You should still be able to access these classes via IL2CPP reflection. - Calling IL2CPP methods can become very performance intensive in math heavy code. For this reason you should avoid Unity's math library as much as possible.
- Use
System.Math
instead ofUnityEngine.Mathf
- Use
System.Numerics.Vector3
instead ofUnityEngine.Vector3
- There are extension methods to convert between the two
Vector3
classes:x.ToNET()
andx.ToIL2CPP()
- Use
- HarmonyX is very useful to work around broken IL2CPP stuff.
GameEvents
is a helper class that let's you get callbacks for common game events. You will most likely need GameEvents.RunOnGameInit(callback)
to be able to hook into game systems after they have been initialized.
ModScripting
lets you register custom MiScript commands. To do this, you must create a new IL2CPP class that implements your commands:
using Il2CppInterop.Runtime.Injection;
using PirateBase;
internal class YourScriptingClass : Il2CppSystem.Object
{
public YourScriptingClass(System.IntPtr ptr) : base(ptr) { }
public YourScriptingClass() : base(ClassInjector.DerivedConstructorPointer<YourScriptingClass>())
{
ClassInjector.DerivedConstructorBody(this);
}
[ModScriptMethod("your-custom-method")]
public string YourCustomMethod()
{
return "Hello World!";
}
[ModScriptMethod("your-other-custom-method")]
public string YourOtherCustomMethod(string _name)
{
return $"Hello {_name}!";
}
}
You must then register an instance of this class on game init with ModScripting.RegisterLibrary(new YourScriptingClass());
Then you can use your methods ingame in any MiScript field like this:
your-custom-method
-> Hello World!
your-other-custom-method Afia
-> Hello Afia!
There are some limitations:
- You cannot have multiple methods with the same command name.
- Your methods cannot be static.
- Your methods will not show up in
list-commands
orhelp
. - You cannot return
IEnumerator
from your methods. - You cannot register custom properties or parsers.
To create a custom component that can be saved ingame, create a new class that inherits from ModSaveable
:
using Il2CppInterop.Runtime.Injection;
using Il2CppInterop.Runtime.Attributes;
using PirateBase;
internal class YourSaveableClass : ModSaveable
{
private float m_savedProperty;
private MiCharacter m_savedReference;
public YourSaveableClass(System.IntPtr ptr) : base(ptr) { }
public YourSaveableClass() : base(ClassInjector.DerivedConstructorPointer<YourSaveableClass>())
{
ClassInjector.DerivedConstructorBody(this);
}
[HideFromIl2Cpp]
protected override void serializeMod(ref ModSerializeHelper _helper)
{
base.serializeMod(ref _helper);
_helper.Serialize(0, m_savedProperty);
_helper.SerializeUnityObject(1, m_savedReference);
}
[HideFromIl2Cpp]
protected override void deserializeMod(ref ModDeserializeHelper _helper)
{
base.deserializeMod(ref _helper);
_helper.Deserialize(0, ref m_savedProperty);
_helper.DeserializeUnityObject(1, ref m_savedReference);
}
}
Override serializeMod()
and deserializeMod()
and add the fields that you want to have saved. Be careful that you use the correct serialize/deserialize methods for your field type. Each field needs to have a (for the type) unique ID assigned to it.
When your plugin is loading you must register your component class with ClassInjector.RegisterTypeInIl2Cpp<YourSaveableClass>()
and ModSaveManager.RegisterType<YourSaveableClass>()
.
When you instantiate your component, it must be parented under a save root:
var go = new GameObject();
go.transform.SetParent(SaveLoadSceneManager.transGetRoot());
go.AddComponent<YourSaveableClass>();
Any assets (e.g. prefabs/materials/etc.) that are instantiated in a saveroot or are referenced in saved components must be loaded on game init with ModModularContainer.Load<T>("addressable_key")
, so that the savesystem knows about them. Unfortunately you also need to manually load any dependencies (e.g. materials on prefab models) of these assets. If this becomes too much work, you could also consider working around this by keeping any models/particle effects/etc. outside of the saveroot and then manually create and destroy these objects when the saved component is created and destroyed.
- It is (probably) not possible to save custom modded classes/structs (except for classes that inherit from UnityEngine.Object).
- It is not possible to save coroutines/
IEnumerator
. - It is not possible to save custom delegates.
- It is also not possible to save callbacks you registered on existing saved game classes, e.g.
MiCharacter.m_evOnDeath
.- You need to remove your callbacks on
GameEvents.beforeSave
and then reregister them onGameEvents.afterSave
.
- You need to remove your callbacks on
ModSerializeHelper
currently does not expose every saveable struct type, e.g.double
orlong
.- You can still save these fields by adding them manually to
_helper.dictFields
.
- You can still save these fields by adding them manually to
With ModUpdate.shouldSkipUpdate
you can check if it is currently safe to execute Update methods on your custom components.
private void Update()
{
if (ModUpdate.shouldSkipUpdate)
return;
// do stuff
}
For some reason UnityEngine.Shader.Find("name")
does not work. Instead you can directly load shaders with the Addressable system, although for most objects you will want you will want to use the "MiStandardMetallic" shaders.
You can access these with ShaderUtility.FindStandardHideVCShader()
and ShaderUtility.FindStandardShowVCShader()
, which will make the viewcone draw behind or infront of your object respectively.
Shadow Gambit checks the file size of its own asset bundles on boot, and refuses to start if they don't match a known value. This makes it difficult to modify these bundles.
You can disable this check either by deleting [game folder]\ShadowGambit_TCC_Data\StreamingAssets\aa\catalog_assetbundle_filesize_cache.json
or by calling ModAddressableManager.disableBundleFileSizeChecks = true
from within the load function of your plugin.