Main

Updated 2 weeks ago

Thulium API — Developer Documentation

mod ID: com.ngeorge.thuliumapi
For: Magicraft (BepInEx Mono)

Add ThuliumAPI as a dependency in your mod's BepInPlugin attribute, then subscribe to the hooks or call the APIs you need. No direct Harmony patching required on your end.


Table of Contents


Setup & Dependency Declaration

In your mod's main plugin class, declare ThuliumAPI as a BepInEx dependency:

using ThuliumAPI;
using BepInEx;

[BepInPlugin("com.yourname.yourmod", "Your Mod", "1.0.0")]
[BepInDependency("com.ngeorge.thuliumapi")]   // ← required
public class MyMod : BaseUnityPlugin
{
    private void Awake()
    {
        // Subscribe to hooks here — they are already patched by ThuliumAPI
        Hooks.OnGameStart += OnGameStart;
        Hooks.OnEnemyDeath += OnEnemyDeath;
    }

    void OnGameStart()  { /* ... */ }
    void OnEnemyDeath(UnitPropertyProxy ppt) { /* ... */ }
}

You do not need to create your own Harmony instance for anything covered by this API.


Hooks — Event System

All hooks live on the static Hooks class. Subscribe and unsubscribe with += / -= like any C# event.

Lifecycle Events

Event Signature When it fires
OnGameStart Action Once, after GameManager.Start()
OnGameUpdate Action Every frame (after GameManager.Update)
OnSceneLoaded Action<string> When a new Unity scene finishes loading; arg = scene name
OnKeyPressed Action<KeyCode> Every frame a key is pressed down
OnDifficultyChanged Action<int> After world difficulty data is saved; arg = new difficulty index
Hooks.OnGameStart += () => Logger.LogInfo("Game started!");

Hooks.OnSceneLoaded += sceneName =>
{
    if (sceneName == "BossRoom") DoSomethingSpecial();
};

Hooks.OnKeyPressed += key =>
{
    if (key == KeyCode.F1) GameAPI.HealPlayer(50f);
};

Hooks.OnDifficultyChanged += diff =>
    Logger.LogInfo($"Difficulty changed to {diff}");

Combat Observation Events

These are read-only — they let you react to combat without altering its outcome.

Event Signature When it fires
OnEnemySpawn Action<UnitPropertyProxy> After an enemy is spawned
OnEnemyDeath Action<UnitPropertyProxy> After a non-player unit dies
OnPlayerDamaged Action<DamageProxy> After the player takes damage
OnEnemyDamaged Action<DamageProxy> After any enemy takes damage
OnPlayerHealed Action<UnitPropertyProxy, float> After HPRecovery is called on the player; second arg = amount
Hooks.OnEnemySpawn += ppt =>
    Logger.LogInfo($"Enemy spawned with {ppt.MaxHP} max HP");

Hooks.OnPlayerDamaged += dmg =>
    Logger.LogInfo($"Player took {dmg.Damage} damage (crit={dmg.IsCritical})");

Hooks.OnPlayerHealed += (ppt, amount) =>
    Logger.LogInfo($"Player healed {amount} HP, now at {ppt.CurrentHP}");

Damage Override Hooks

These fire before damage is applied. Return false to cancel the damage entirely.
You can also mutate proxy.Damage to change the value before it lands.

Event Signature
OnBeforePlayerDamage Func<DamageProxy, bool>
OnBeforeEnemyDamage Func<DamageProxy, bool>
// Make player immune to all trap damage
Hooks.OnBeforePlayerDamage += dmg =>
{
    if (dmg.IsTrapDamage) return false; // cancel it
    return true;                         // let it through
};

// Double all enemy incoming damage
Hooks.OnBeforeEnemyDamage += dmg =>
{
    dmg.Damage *= 2f;
    return true;
};

Note: Every registered handler is called in order. If any returns false, damage is cancelled and remaining handlers are skipped.


Spawn Interception Hooks

Fires before an enemy unit is spawned. Return false to cancel the spawn entirely.
You may also redirect proxy.Position to move where the unit spawns.

Event Signature
OnBeforeEnemySpawn Func<SpawnRequestProxy, bool>
// Block a specific enemy type from ever spawning
Hooks.OnBeforeEnemySpawn += req =>
{
    if (req.UnitID == 42) return false; // suppress enemy #42
    return true;
};

// Redirect all spawns to a fixed point
Hooks.OnBeforeEnemySpawn += req =>
{
    req.Position = new Vector3(0f, 0f, 0f);
    return true;
};

Proxy Types

Proxies are lightweight wrappers around live game objects. They cache reflection internally so you pay the cost only once.

UnitPropertyProxy

Wraps UnitProperty — the stats component attached to every unit (player and enemies).

Member Type Description
CurrentHP float (get/set) The unit's current HP
MaxHP float (get) The unit's maximum HP from its config
IsDead bool (get) Whether the unit is considered dead
IsInvincible bool (get) Whether invincibility frames are active
GameObject GameObject The underlying GameObject
Transform Transform The unit's Transform
SetInvincible(bool) void Grant or remove invincibility frames
Hooks.OnEnemySpawn += ppt =>
{
    // Print stats of newly spawned enemy
    Logger.LogInfo($"{ppt} spawned at {ppt.Transform.position}");
    // Halve its HP immediately
    ppt.CurrentHP = ppt.MaxHP * 0.5f;
    // Make it briefly invincible
    ppt.SetInvincible(true);
};

DamageProxy

Wraps TakeDamageInfo — represents a single incoming hit. Available in both OnBefore*Damage (mutable) and On*Damaged (read) hooks.

Member Type Description
Victim UnitPropertyProxy The unit receiving the hit
Damage float (get/set) Damage amount; set this to change it
IsCritical bool (get/set) Whether the hit is a critical
ImmuneToThisHit bool (get/set) Flag the hit as immune (no damage number shown)
IsPercentageDamage bool (get) True if damage is a % of max HP
IsTrapDamage bool (get) True if the source is a trap
KnockbackForce Vector3 (get/set) Knockback applied to the victim
SuppressKnockback() void Sets KnockbackForce to zero
Hooks.OnBeforePlayerDamage += dmg =>
{
    // Halve all non-crit damage to the player
    if (!dmg.IsCritical) dmg.Damage *= 0.5f;
    // Never get knocked back
    dmg.SuppressKnockback();
    return true;
};

SpawnRequestProxy

Represents an incoming enemy spawn request. Available inside OnBeforeEnemySpawn.

Member Type Description
UnitID int (get) The unit config ID of the enemy being spawned
Position Vector3 (get/set) World position; set to redirect where it spawns
Hooks.OnBeforeEnemySpawn += req =>
{
    Logger.LogInfo($"Spawning unit {req.UnitID} at {req.Position}");
    return true;
};

GameAPI — Common Game Operations

A static surface for the most common game operations. No reflection required on your side.

Singletons

object battleMgr = GameAPI.BattleMgr;
object playerMgr = GameAPI.PlayerMgr;
object gameMgr   = GameAPI.GameMgr;

These return the live singleton instances. Cast them or use Utils.Call / Utils.GetField for further access.


World & Difficulty

int diff = GameAPI.GetDifficulty();   // 0, 1, 2, …
GameAPI.SetDifficulty(2);             // force difficulty index 2
object worldData = GameAPI.GetWorldData(); // raw WorldData object

Player

// Get the player GameObject
GameObject player = GameAPI.GetPlayer();

// Get a proxy for reading/writing player stats
UnitPropertyProxy ppt = GameAPI.GetPlayerProperty();
Logger.LogInfo($"Player HP: {ppt.CurrentHP} / {ppt.MaxHP}");

// Manipulate HP
GameAPI.HealPlayer(100f);          // call the game's own recovery method
GameAPI.SetPlayerHP(1f);           // directly set the HP field

// Position
Vector3 pos = GameAPI.GetPlayerPosition();
GameAPI.TeleportPlayer(new Vector3(10f, 0f, 5f));

// Kill the player (triggers the normal death flow)
GameAPI.KillPlayer();

// If the player object gets replaced (e.g., scene reload), clear the cache
GameAPI.InvalidatePlayerCache();

Enemies

// Get all live enemy GameObjects
GameObject[] enemies = GameAPI.GetAllEnemies();

// Get wrapped proxies for all enemies
UnitPropertyProxy[] proxies = GameAPI.GetAllEnemyProperties();
foreach (var e in proxies)
    Logger.LogInfo($"Enemy HP: {e.CurrentHP}/{e.MaxHP}");

// Instantly destroy all enemies (bypasses death logic)
GameAPI.KillAllEnemies();

// Kill all enemies through the game's own death system (triggers drops, events, etc.)
GameAPI.KillAllEnemiesViaGame();

ContentRegistry — Mod Integration Layer

ContentRegistry is the preferred way to register persistent behaviours rather than subscribing directly to raw hooks. It handles pipeline ordering internally.

Damage Modifiers

Registered modifiers run on every incoming hit and may return a new damage value.

// Reduce all player-incoming damage by 20%
ContentRegistry.RegisterPlayerDamageModifier(dmg =>
{
    return dmg.Damage * 0.8f;
});

// Multiply enemy-incoming damage by 1.5×
ContentRegistry.RegisterEnemyDamageModifier(dmg =>
{
    return dmg.Damage * 1.5f;
});

Multiple modifiers chain — each receives the output of the previous one.


Spawn Filters

Filters are evaluated in registration order. Returning false from any filter suppresses the spawn.

// Prevent two specific enemy IDs from ever spawning
ContentRegistry.RegisterSpawnFilter(req =>
{
    return req.UnitID != 7 && req.UnitID != 13;
});

Per-Unit Death Callbacks

Fire a callback whenever a unit with a specific ID dies, without polluting your main OnEnemyDeath handler.

// React specifically to unit #5 dying
ContentRegistry.OnUnitDeath(5, ppt =>
{
    Logger.LogInfo("The mini-boss died!");
    GameAPI.HealPlayer(50f); // reward the player
});

Custom Prefab Catalogue

Register your own prefabs so other mods (or your own systems) can spawn them by name.

// Registration — typically in Awake after asset loading
ContentRegistry.RegisterPrefab("mymod.explosion", myExplosionPrefab);

// Spawning anywhere
if (ContentRegistry.HasPrefab("mymod.explosion"))
{
    ContentRegistry.SpawnRegisteredPrefab(
        "mymod.explosion",
        new Vector3(5f, 0f, 3f),
        Quaternion.Euler(0, 45f, 0)  // optional rotation
    );
}

Utils — Helpers & Shortcuts

General-purpose utility methods that don't belong to a specific system.

// Logging
Utils.Print("info message");
Utils.Warn("warning message");
Utils.Error("error message");

// Reflection helpers — read/write arbitrary fields without boilerplate
float someValue = Utils.GetField<float>(someObject, "fieldName");
Utils.SetField(someObject, "fieldName", newValue);

// Call any method by name
object result = Utils.Call(someObject, "MethodName", arg1, arg2);

// Time manipulation
Utils.PauseGame();           // timeScale = 0
Utils.ResumeGame();          // timeScale = 1
Utils.SetTimeScale(0.5f);    // slow motion

// Spawn an arbitrary prefab
GameObject go = Utils.SpawnPrefab(myPrefab, new Vector3(1, 0, 1));

// Find enemies
GameObject[] enemies = Utils.GetAllEnemies();

// Get the main Canvas component
Component canvas = Utils.GetMainCanvas();

Full Mod Example

A complete minimal mod that demonstrates several features together:

using BepInEx;
using BepInEx.Logging;
using ThuliumAPI;
using UnityEngine;

[BepInPlugin("com.yourname.examplemod", "Example Mod", "1.0.0")]
[BepInDependency("com.ngeorge.thuliumapi")]
public class ExampleMod : BaseUnityPlugin
{
    static ManualLogSource Log;
    int enemiesKilledThisRun = 0;

    private void Awake()
    {
        Log = Logger;

        // --- Lifecycle ---
        Hooks.OnGameStart += () =>
        {
            enemiesKilledThisRun = 0;
            Log.LogInfo("New run started — counters reset.");
        };

        Hooks.OnSceneLoaded += scene =>
            Log.LogInfo($"Scene loaded: {scene}");

        Hooks.OnKeyPressed += key =>
        {
            if (key == KeyCode.F2) GameAPI.HealPlayer(999f);
            if (key == KeyCode.F3) GameAPI.KillAllEnemiesViaGame();
        };

        // --- Damage ---
        // Player takes 25% less damage
        ContentRegistry.RegisterPlayerDamageModifier(dmg => dmg.Damage * 0.75f);

        // All enemies take 10% more damage
        ContentRegistry.RegisterEnemyDamageModifier(dmg => dmg.Damage * 1.1f);

        // Block all trap damage to the player
        Hooks.OnBeforePlayerDamage += dmg =>
        {
            if (dmg.IsTrapDamage)
            {
                Log.LogInfo("Trap damage blocked!");
                return false;
            }
            return true;
        };

        // --- Spawns ---
        // Block enemy ID 99 from ever spawning
        ContentRegistry.RegisterSpawnFilter(req => req.UnitID != 99);

        // Log every spawn
        Hooks.OnEnemySpawn += ppt =>
            Log.LogInfo($"Enemy spawned — HP {ppt.CurrentHP}/{ppt.MaxHP}");

        // --- Deaths ---
        Hooks.OnEnemyDeath += ppt =>
        {
            enemiesKilledThisRun++;
            Log.LogInfo($"Enemy killed #{enemiesKilledThisRun} this run.");
        };

        // React specifically to unit #5
        ContentRegistry.OnUnitDeath(5, ppt =>
            Log.LogInfo("The elite enemy died! Rewarding player."));

        Log.LogInfo("Example Mod loaded.");
    }
}

Thulium API is an open modding utility for Magicraft. All hook targets are resolved at runtime against the live game assembly — if a patch target is missing, a warning is logged and that hook is silently skipped.

Pages