Main
Updated 2 weeks agoThulium API — Developer Documentation
mod ID: com.ngeorge.thuliumapi
For: Magicraft (BepInEx Mono)
Add
ThuliumAPIas a dependency in your mod'sBepInPluginattribute, 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
- Hooks — Event System
- Proxy Types
- GameAPI — Common Game Operations
- ContentRegistry — Mod Integration Layer
- Utils — Helpers & Shortcuts
- Full Mod Example
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.