You are viewing a potentially older version of this package. View all versions.
cheesasaurus-HookDOTS_API-1.0.0 icon

HookDOTS API

Provides an API for mods to hook Unity DOTS systems.

Date uploaded 3 weeks ago
Version 1.0.0
Download link cheesasaurus-HookDOTS_API-1.0.0.zip
Downloads 73
Dependency string cheesasaurus-HookDOTS_API-1.0.0

This mod requires the following mods to function

BepInEx-BepInExPack_V_Rising-1.733.2 icon
BepInEx-BepInExPack_V_Rising

BepInEx pack for V Rising. Preconfigured and includes Unity Base DLLs.

Preferred version: 1.733.2

README

HookDOTS API

HookDOTS is a modding library for hooking Unity DOTS games. By itself, it doesn't do anything.

Why use HookDOTS?

Harmony is an excellent tool, but it fails at one important thing: Hooking unmanaged systems in IL2CPP builds.
HookDOTS was created to solve this problem.

Notable attributes provided (for plugin developers):

  • [EcsSystemUpdatePrefix(typeof(EquipItemSystem))]
  • [EcsSystemUpdatePostfix(typeof(EquipItemSystem))]
  • [WhenCreatedWorldsContainAny("Server", "Client_0")]

Installation

  1. Install BepInEx
  2. Extract HookDOTS.API.dll into (VRising folder)/BepInEx/plugins

Usage

For Server Operators

By itself, HookDOTS doesn't do anything. It is required by other plugins and should be kept up to date.

For Plugin Developers

How to use

1. Add a reference to the plugin.

dotnet add package HookDOTS.API -v 1.*

2. Add the API plugin as a dependency via the BepInDependency attribute on your plugin class.

[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
[BepInDependency("HookDOTS.API")]
public class ExamplePlugin : BasePlugin

3. Register hooks in your plugin's Load method.

public override void Load()
{
    //... other initialization code

    // Register your plugin's hooks with HookDOTS
    _hookDOTS = new HookDOTS.API.HookDOTS(MyPluginInfo.PLUGIN_GUID, Log);
    _hookDOTS.RegisterAnnotatedHooks();
}

4. Teardown in your plugin's Unload method.

public override bool Unload()
{
    //... other unloading code

    _hookDOTS.Dispose();
    return true;
}

5. Set up prefix and postifx hooks for System OnUpdate calls. Both managed and unmanaged systems can be hooked.

public class ExamplePatch
{
    // The hook must be `public` and `static`.
    // If you want SystemState* passed in, an `unsafe` context is also required.
    [EcsSystemUpdatePrefix(typeof(EquipItemSystem))]
    unsafe public static bool ExamplePrefix(SystemState* systemState)
    {
        var world = systemState->World;
        ExamplePlugin.LogInstance.LogInfo($"ExamplePrefix executing in world {world.Name}.");

        // You can return false, to skip the hooked OnUpdate. Other prefix hooks will still run.
        bool shouldSkipTheOriginal = true;
        return !shouldSkipTheOriginal; // this will be returned, and is false. Therefore the original will be skipped.
    }

    [EcsSystemUpdatePostfix(typeof(EquipItemSystem))]
    unsafe public static void ExamplePostfix(SystemState* systemState)
    {
        var world = systemState->World;
        ExamplePlugin.LogInstance.LogInfo($"ExamplePostfix executing in world {world.Name}.");
        // Unlike a prefix hook, a postfix hook cannot return false to skip anything.
        // The only valid return type is `void`.
    }

}

onlyWhenSystemRuns

You can set onlyWhenSystemRuns to false, and the hook will be called even if the system doesn't actually run.

[EcsSystemUpdatePrefix(typeof(EquipItemSystem), onlyWhenSystemRuns: false)]
public static void ExamplePrefix2()
{
    // (this is commented out, because the log would be spammed every frame)
    // ExamplePlugin.LogInstance.LogInfo($"ExamplePrefix2 executing");
}

Throttling

If you're using hooks to dump information and don't want to be flooded every frame, the Throttle attribute can be used.

// You can specify `days`, `hours`, `minutes`, `seconds`, and `milliseconds` to define the throttle interval.
// Internally, these are used to create a `System.TimeSpan`
[Throttle(seconds: 10)]
[EcsSystemUpdatePrefix(typeof(EquipItemSystem), onlyWhenSystemRuns: false)]
public static void ExamplePrefixThrottled()
{
    ExamplePlugin.LogInstance.LogInfo($"ExamplePrefixThrottled executing (throttled to once every 10 seconds)");
}

Alternative ways to register hooks

Procedurally, using a HookRegistrar:

var hookDOTS = new HookDOTS.API.HookDOTS(MyPluginInfo.PLUGIN_GUID, Log);
//...
var hook = HookDOTS.Hooks.System_OnUpdate_Prefix.CreateHook(MyHookMethod);
hookDOTS.HookRegistrar.RegisterHook_System_OnUpdate_Prefix<TakeDamageInSunSystem_Server>(hook);

Builder style, using SetupHooks:

var hookDOTS = new HookDOTS.API.HookDOTS(MyPluginInfo.PLUGIN_GUID, Log);
//...
hookDOTS
    .SetupHooks()
        .BeforeSystemUpdates<EquipItemSystem>()
            .ExecuteDetour(MyMethodA).Always()
            .And()
            .ExecuteDetour(MyMethodB).Always()
    .Also()
        .AfterSystemUpdates<TakeDamageInSunSystem_Server>()
            .ExecuteAction(MyMethodC).Throttled(seconds: 2)
    .Also()
        .BeforeSystemUpdates<DropInventoryItemSystem>(onlyWhenSystemRuns: false)
            .ExecuteDetour(MyMethodD).Throttled(seconds: 5)
    .RegisterChain();
    // be sure to call RegisterChain! Otherwise the entire chain will be discarded.

World Readiness triggers for Initialization

There are world readiness checks/triggers if you need to defer something until after worlds are created.

Worlds are immediately checked during hook registration, in case they were already set up.
Each registered hook will only be executed once. (Even if it doesn't complete, e.g. due to errors)

// The hook must be `public` and `static`.
[WhenCreatedWorldsContainAny("Server", "Client_0")]
unsafe public static void ExampleInitializer1(IEnumerable<World> worlds)
{
    ExamplePlugin.LogInstance.LogInfo($"{worlds.First().Name} world is ready.");
}

// there is also an "All" version (instead of "Any")
[WhenCreatedWorldsContainAll("Server", "Default World")]
public static void ExampleInitializerAll()
{
    ExamplePlugin.LogInstance.LogInfo($"ExampleInitializerAll executing.");
}

Builder style, using SetupHooks:

hookDOTS
    .SetupHooks()
        .WhenCreatedWorldsContainAny(["Server", "Default World"])
            .ExecuteActionOnce(LogSomething)
            .And()
            .ExecuteActionOnce(DeferredInitialize)
    .RegisterChain();
    // be sure to call RegisterChain! Otherwise the entire chain will be discarded.

Example project

A full example project is available. Of particular interest:

Support

Join the modding community.

Post an issue on the GitHub repository.

CHANGELOG

1.0.0

  • Initial release