RealmApi
Modding API for Stolen Realm with UI helpers and opt-in observational hooks. Hooks are disabled by default and require an exact game version and assembly hash match.
| Last updated | 11 hours ago |
| Total downloads | 2 |
| Total rating | 0 |
| Categories | Mods |
| Dependency string | DefRuivo_StolenRealmMods-RealmApi-0.1.0 |
| Dependants | 0 other packages depend on this package |
This mod requires the following mods to function
BepInEx-BepInExPack
BepInEx pack for Mono Unity games. Preconfigured and ready to use.
Preferred version: 5.4.2305README
RealmApi
Modding API for Stolen Realm, with UI helpers and optional observational hooks. It is a base for other mods; it does not add playable content on its own.
- Version: 0.1.0
- Author: gumatos
- Category: Mods (
mods,stolen-realmcommunity) - BepInEx GUID:
com.gumatos.realmapi - Namespace:
Gumatos.RealmApi - License: MIT
Pre-release in preparation. The implementation was exercised offline, not validated inside the game process in this delivery. The hooks ship disabled. The packer already assembles the ZIP with the three assemblies together in
plugins/RealmApi/; this page does not announce a published package.
What it does
- Provides a documented, versioned surface for other mods of this repository: one place for the generic UI plumbing (cloning a native game object, wiring a button click) and for reading game properties by reflection.
- Ships an observational hook catalog (
HookCatalog) that observes four game points and never changes results or arguments: every postfix is observational. - Ships an UI family (
Gumatos.RealmApi.UI) that can mutate the game UI behind explicit binding and opt-in (see UI family below). - Publishes a notification bus with disposable subscription and isolation of subscriber failures.
What it does not do
- It does not add playable content on its own: it is a base for other mods.
- It does not implement the inventory domains outside the four hooks above and the UI family (detailed combat, enemies, quests, persistent data) — there is no silent stub and no compatibility promise.
- It does not cover TextMeshPro or the full filling of a tooltip (both are listed as not exercised below).
- It does not discover, on its own, where the game keeps each host: the live host for each UI family is
supplied by the consumer through
UiHosts.
For players: installation planned after release
Install only when a mod you use asks for this dependency and there is a released version.
Declared requirement: BepInEx-BepInExPack-5.4.2305. Use the Stolen Realm community mod manager.
The complete package must place the three files together:
BepInEx/plugins/RealmApi/
RealmApi.dll
RealmApi.Core.dll
RealmApi.Hooks.dll
Do not copy RealmApi.dll alone: the plugin depends on Core and Hooks. Avoid old copies in any
subfolder of plugins, because BepInEx also looks for them recursively. To remove it, uninstall through
the manager and review the mods that depend on the API; in a manual installation, remove only the
RealmApi folder. Do not keep DLL backups inside plugins.
Configuration file: BepInEx/config/com.gumatos.realmapi.cfg, section Safety:
[Safety]
EnableHooks = false
false is the default: the plugin registers that the hooks are disabled and does not install them. Enable
it only in an authorized in-game test. With true, the plugin still requires the exact version and hash of
the target listed below. Check BepInEx/LogOutput.log for the reason of a refusal, instead of disabling
the checks.
That gate controls the hooks installed by the plugin, not the whole UI family. A consumer can bind
the UI and authorize mutations explicitly, even with EnableHooks=false.
The official 256×256 icon (art provided by the author) ships with the package as icon.png. The
preparation of this version neither installed into the profile nor published the package.
Target game version (mandatory, fail-closed)
The five fields below make up the declared identity of the target. The runtime gate (ApiIdentity.Matches,
api/Core/Contracts.cs:24-26) checks only two of them at execution time: Application.version (≈ game version)
and the SHA-256 of Assembly-CSharp.dll. The others (Steam AppID, Steam Build, Unity) are documented identity of the target
and are used in the update step (api/docs/target-game.json) — they are not compared at runtime.
BepInProcess("Stolen Realm.exe") matches the executable name, not the AppID.
| Field | Value | Checked at runtime? | How it was measured |
|---|---|---|---|
| Steam AppID | 1330000 |
NO (documented identity) | game Steam manifest |
| Game version | 1.3.1 |
YES (Application.version) |
PlayerSettings in <game>/Stolen Realm_Data/globalgamemanagers |
| Steam Build | 25240684 |
NO (documented identity) | buildid in appmanifest_1330000.acf |
| Unity | 2022.3.62f2 |
NO (documented identity) | header of globalgamemanagers |
| Assembly-CSharp SHA-256 | 8bb4a7c512da20770a81dfac0347e9a68a8c0eff736f4566c52a66923b37855a |
YES (sha256sum) |
sha256sum lib/Assembly-CSharp.dll (same as the game's Managed/Assembly-CSharp.dll) |
The values live in Gumatos.RealmApi.ApiIdentity (api/Core/Contracts.cs). Detail and reproducibility:
api/docs/target-game.json. If the version (Application.version) or the SHA-256 of the Assembly-CSharp
diverge, the API installs no hook at all
(ApiIdentity.Matches compares only those two fields and refuses when they are missing or different).
Existing consumers in the project
These are concrete uses in the code, not a claim that the published versions of the consumers already carry those integrations:
- RoguelikeSkillTreeVisualizer (RSTV): uses
UIFactory.CloneandUIFactory.BindClickthrough an explicit binding. TextMeshPro and specific visual assembly stay inside the mod itself. - RoguelikeDebugger: probes
ApiIdentityby reflection, optionally; it keeps a local identity without the API or with the hooks off. - RealmApi → KitSdk bridge: a separate observational adapter, explicitly created by the host with a bus, producer state, session and diagnostics. It does not discover the API and does not install hooks.
These consumers do not ship with this package. The API does not depend on them to load.
For mod authors
The following sections detail the surface and its limits. Paths api/, lib/ and docs/ are
references to the repository tree,
not files promised inside the ZIP. Inspection and smoke tools also live in the repository;
the runtime package needs only the three assemblies of its own plus the distribution metadata.
How to use
Consumer: reference RealmApi.Core.dll (and, if you need to install hooks, RealmApi.Hooks.dll; for the
UI family, RealmApi.dll, which is where its 5 public types live). The hooks part observes and does not
change a result or an argument of the game — every postfix is observational. The UI family, on the contrary, mutates
the UI (see its own section below).
// 1) Observe the catalog points. Runs on the hook thread; the return value is the subscription.
IDisposable sub = RealmHooks.Observed.Subscribe(n =>
{
// n.HookId -> "combat.turn-started", "ui.localized", ...
// n.Instance -> object instance when the target is not static (may be null)
// n.Arguments -> copy of the argument list (not a deep copy of Unity objects)
// n.Result -> return value, when the target returns a value
});
// 2) One-off property read, without reflection spread across your mod:
object? valor = RealmAccess.ReadProperty(assembly, "Burst2Flame.Game", "Instance");
Hooks implemented in the 0.1.0 base (HookCatalog.All, four only):
| HookId | Target | Shape |
|---|---|---|
game.awake |
Burst2Flame.Game::Awake |
void postfix, instance |
ui.skill-tree.updated |
SkillTreeManager::UpdateSkillTreeItems |
void postfix, instance |
ui.localized |
OptionsManager::Localize (static) |
postfix with __result (string) |
combat.turn-started |
GameLogic::StartNewTurn |
void postfix, instance |
The inventory families outside the four hooks above and the UI family (detailed combat, enemies, quests, persistent data) are not implemented — there is no silent stub and no promise of compatibility.
UI family (API-R1-I2) — behavioural, through explicit binding
Gumatos.RealmApi.UI has the 5 inventory types with the exact signatures (UIFactory, ActionBarUI,
SkillTreeUI, MenuUI, TooltipHelper; 21 signatures across 5 types, including the double Clone overload).
It is different in nature from the hooks: AddButton, AddTab, BindClick, SetText, Clone, Destroy
etc. MUTATE the game UI. That is why it does not go through the observational bus (RealmHooks.Observed),
it is not a Harmony postfix, and it lives behind three explicit locks:
- Explicit binding.
UiFamily.Bind(assemblyDoJogo, hosts, allowMutation)resolves, by exact metadata (the same rule asHookDescriptor.Resolve), the 17 requirements of the family against the game assembly: missing target ->MissingMemberException; divergent signature ->MissingMethodException; host of the wrong type ->ArgumentException. A binding is only published when all the requirements resolve: there is no partial binding and aBindthat fails does not change the current binding. Calling any method of the 5 families without a binding ->InvalidOperationException. - Mutation opt-in. Without
allowMutation:trueinBind, every method that mutates throws. The read-only ones (HasTab,FindChild) are the ones that do not require the opt-in, but they still require a binding and the thread. - Thread. The binding records the thread (the Unity main thread) and using the family from another thread throws: Unity UI is not thread-safe.
// True state of the game that the consumer supplies (what it found in the scene); the API does NOT guess where
// the game keeps each thing. Missing host = that family fails closed.
var hosts = new UiHosts {
GuiManager = guiManagerDoJogo, // UIFactory + TooltipHelper
CurrentCharacterUi = currentCharacterUi, // ActionBarUI
SkillTreeManager = skillTreeManager, // SkillTreeUI
MenuTabManager = menuTabManager // MenuUI
};
UiFamily.Bind(assemblyDoJogo, hosts, allowMutation: true);
try {
UIFactory.SetText(botaoNativo, "Meu texto"); // mutates
ActionBarUI.AddButton("meu-botao", icone, AoClicar); // mutates
bool tem = SkillTreeUI.HasTab(SkillTree.Fire); // read
} finally { UiFamily.Unbind(); } // clears the binding and the state of the 5 families
Target table (all measured in lib/Assembly-CSharp.dll, verified on every smoke run by the test
actual-game-ui-requirements-metadata):
| Type | Game target (exact signature) | What the API does |
|---|---|---|
TooltipHelper |
Tooltip::HideTooltip(), through GUIManager::instance + field GUIManager::tooltip |
HideTooltip delegates 1:1 to the native member |
ActionBarUI |
CurrentCharacterUI::Instance/ActiveSkillbar, Skillbar::HasAvailableSlot(), Skillbar::RefreshSkillBar() |
proves a free slot on the host, registers the button, asks for the bar to be rebuilt |
SkillTreeUI |
SkillTreeManager::IsNotNullAndIsActive, SkillTreeManager::UpdateSkillTreeItems() |
proves the tree is active, registers the tab, asks for a rebuild |
MenuUI |
MenuTabManager::RefreshSelected() |
registers the tab and asks for a rebuild |
UIFactory |
GUIManager::instance |
clones/copies layout/wires click/text/icon/destroys over the native Unity objects |
Why the 5 types live in RealmApi.dll and not in Core. Measured: Core is netstandard2.0 and the
target's Unity assemblies are netstandard2.1 — the direct reference fails (CS1705) and a net472 process
does not even load UnityEngine.CoreModule (FileNotFoundException: netstandard, Version=2.1.0.0).
Signing the 5 types with Unity types (as the inventory requires) is only possible in a netstandard2.1 project, which is
the plugin's. That is why no new project was created (the acceptance criteria speak of 5 Release projects): what
can be exercised offline (binding, guards, state, delegation) stayed in Core; the Unity signatures stayed in
RealmApi.dll. Conformance of the 21 signatures is proven by metadata, not by execution
(api/tools/check_ui_contract.py -> api/docs/ui-contract-diff.json, verdict CONFORME, with a negative control
that fails if the comparison passes by omission).
Config and safety
In BepInEx/config, section Safety:
EnableHooks(default false): hooks disabled until an in-game smoke is explicitly authorized. Withfalsethe plugin only writes to the log and installs nothing.- With
true, the plugin still requires that the version (Application.version) and the SHA-256 of theAssembly-CSharpof the current process match the table above; any divergence becomes aLogErrorand zero hooks.
How to build
Prerequisites:
- .NET SDK 6.0 (measured:
6.0.428). - Reference DLLs in
lib/(Assembly-CSharp.dll,0Harmony.dll,BepInEx.dll,UnityEngine.dll,UnityEngine.CoreModule.dll). Used by the plugin (RealmApi.csproj) and by Core/Hooks. - BepInEx Core (r2modman profile):
$(USERPROFILE)/AppData/Roaming/r2modmanPlus-local/StolenRealm/profiles/Default/BepInEx/core. Smoke (RealmApi.Smoke.csproj) and Inspect (Inspect.csproj) read DLLs from there through the propertyBepInExCoreDir(default: the same path; it can be overridden with-p:BepInExCoreDir=<path>). Smoke consumes:0Harmony.dll,Mono.Cecil.dll,MonoMod.RuntimeDetour.dll,MonoMod.Utils.dll,BepInEx.dll. Inspect consumes:Mono.Cecil.dll. - UnityEngine.dll*: they come from
lib/, not from BepInEx Core.
dotnet build api/Core/RealmApi.Core.csproj -c Release
dotnet build api/Hooks/RealmApi.Hooks.csproj -c Release
dotnet build api/Plugin/RealmApi.csproj -c Release
dotnet build api/Smoke/RealmApi.Smoke.csproj -c Release
dotnet build api/tools/Inspect/Inspect.csproj -c Release
Note: to build Smoke/Inspect without the r2modman profile installed, pass
-p:BepInExCoreDir=<path-to-core>pointing at a folder containing the DLLs listed above.
Nothing is installed by default. api/Plugin/RealmApi.csproj has a deploy target DeployToBepInEx
(API-R1-I1) that follows the opt-in rule of the root (Directory.Build.props): building without
-p:DeployToBepInEx=true copies no file at all into the r2modman profile; with the flag, it copies the three
assemblies the plugin needs, into the same folder (RealmApi.dll, RealmApi.Core.dll, RealmApi.Hooks.dll).
A deploy rehearsal into an isolated folder, the install command, the success/failure criteria and the rollback are in
docs/automacao/API-R1-runbook-runtime.md. Measured in this round: the default build (without the flag) did not change
a byte of the profile — a fingerprint before/after in docs/automacao/API-R1-I1-evidencias/.
How to run the smoke suite (offline)
api/Smoke/bin/Release/net472/RealmApi.Smoke.exe \
C:/dev/stolen-realm/lib/Assembly-CSharp.dll \
C:/dev/stolen-realm/api/Plugin/bin/Release/netstandard2.1/RealmApi.dll
Result measured in this delivery: Ran 62 tests; failures: 0, exit 0 (api/docs/smoke-result.json, literal
output in api/docs/smoke-output.txt). The base had 25; API-R1-I2 added 37, 5 of them cases per type
of the UI family (target-present, target-absent, member-absent, signature-divergent,
unbound-fails-closed) plus real delegation in the CLR, the mutation opt-in, thread, missing/wrong host,
atomic Bind and the test actual-game-ui-requirements-metadata (17 requirements checked against the
installed Assembly-CSharp).
It answers the three questions of the acceptance criteria: does it load? (Core/Hooks load and the identity is checked),
does it expose? (the catalog resolves type+exact signature and the plugin attribute matches), do the hooks respond?
(real Harmony postfixes fire, isolate subscribers, preserve third-party owners and are removed
leaving zero patch) — and, for the UI family, does the target exist? (fail-closed binding, delegation and guards).
The Unity targets of the smoke are artificial fixtures with the same signatures: they prove the mechanics in the CLR,
they do not prove game behaviour. The smoke CLR (net472) does not load RealmApi.dll (netstandard2.1);
that is why the 5 public types are checked by metadata (Inspect ui + api/tools/check_ui_contract.py) and the smoke
only checks presence/absence of a stub by metadata (ui-public-surface-in-plugin-metadata).
How to update when the game changes
Fixed order; every step has a reproducible command.
- Re-check the identity. Run
sha256sum lib/Assembly-CSharp.dlland compare it withApiIdentity.AssemblySha256; checkbuildidinappmanifest_1330000.acfand the version inglobalgamemanagers. Updateapi/docs/target-game.jsonandApiIdentityonly if the target really changed. While version and hash do not match, the plugin refuses to install hooks — that is intentional. - Regenerate the surface.
dotnet run --project api/tools/Inspect/Inspect.csproj -c Release -- targets lib/Assembly-CSharp.dll > api/docs/game-targets.json. The command reads metadata (Mono.Cecil); it does not execute the game assembly. - Re-compare. For every
HookDescriptorof the catalog, confirm that the type exists, that the method has an identical signature and that the shape of the postfix is still supported (IsStatic/ReturnType). The smoke already covers that by metadata (actual-game-hook-metadata). For the UI family, the same step is the testactual-game-ui-requirements-metadata, which resolves the 17 requirements ofUiRequirements.Allagainst the measuredAssembly-CSharp. - Adjust your own code. If a target changed name/signature, fix the
HookCatalog(hooks) orUiRequirements(api/Core/Ui/UiFamily.cs, UI family) and, if the semantics of the point changed, the hook or the corresponding method. If the point disappeared, remove it from the catalog and record the removal indocs/automacao/API-R1-*.md— do not invent a silent alternative target. - Rebuild and run the smoke. The five
dotnet buildcommands above and the suite; requirement:failures: 0. - Re-check the conformance of the UI family. Regenerate the implemented surface and run the checker:
dotnet run --project api/tools/Inspect/Inspect.csproj -c Release --no-build -- ui api/Plugin/bin/Release/netstandard2.1/RealmApi.dll api/Core/bin/Release/netstandard2.0/RealmApi.Core.dll > api/docs/ui-contract.jsonand thenpython api/tools/check_ui_contract.py(requirement:CONFORME, exit 0, with the negative control reporting a problem — if the negative control passes, the comparison is worthless). - Re-record the evidence. Update
api/docs/target-game.json,api/docs/smoke-result.json,api/docs/smoke-output.txtandapi/docs/ui-contract-diff.jsonwith the new round. Estimated numbers do not belong here. - Runtime only with authorization. An in-game smoke (real BepInEx) is a separate act, explicitly authorized by the owner; until then the result stays declared as NOT exercised.
How to contribute
- A new hook = one method in the catalog + one metadata test + one case in the real dispatch smoke.
If the required shape is neither
void(instance) norstring(static), that is a shape extension and needs an architecture decision before the code. - A new member of the UI family = one
UiRequirementin the catalog (api/Core/Ui/UiFamily.cs) + the method in the public type with the inventory signature + one case in the smoke. A member that mutates the UI must enter the list that requiresallowMutation:true; a member that only reads does not. Signature conformance is checked byapi/tools/check_ui_contract.py, which fails on a divergence of name, type, return, order/type/name of parameter and optionality — it is not a review by reading. - Do not change
Assembly-CSharp.dlland do not redistribute it. Never reference the game assembly in the plugin code (the smoke has the testno-game-assembly-dependencyto prevent that regression). - No behaviour-modifying patches in the hooks of this base: the 0.1.0 hooks are observational. The UI family is behavioural and may mutate the interface with a binding and the opt-in; cancelling/substituting arguments from the hooks is a separate front.
- Every statement in a document of this module must come with a file path, a count or a reproducible
command; whatever is not exercisable declares itself
NAO_EXERCITADOwith the reason. - Language: the package pages (this README and the CHANGELOG) are in English, with a short Portuguese section at the
end of this README — the same shape as the other packages of this project. Internal documentation under
docs/stays in Portuguese.
NOT exercised in this delivery
- Loading by BepInEx inside the game process, gameplay, turns/saves/multiplayer.
- Runtime identity: NAO_EXERCITADO.
ApiIdentity.Matches(which comparesApplication.version+ the SHA-256 ofAssembly-CSharp) never ran inside the real game process — the offline smoke checks metadata, not the Unity runtime. Version equality will only be exercised when an authorized in-game smoke is run. - Installation into the
r2modmanprofile: NAO_EXERCITADO — the deploy target exists and is conditioned on the opt-in (seedocs/automacao/API-R1-runbook-runtime.md), but nothing was copied into the profile: in this round the profile fingerprint (path/bytes/mtime/sha256 ofplugins/andconfig/) is identical before and after the build (docs/automacao/API-R1-I1-evidencias/01and04, the hash of the set equal in both). - The inventory families outside the four hooks above and the UI family.
- UI family — runtime: nothing was called inside the game.
NAO_EXERCITADO: (a) real visual composition (UnityEngine.Object.Instantiate,Transform.Find,Image.sprite,Text.text,LayoutGroup,Destroy) runs only on metadata fixtures; (b) the native filling of the tooltip (Tooltip::ShowTooltipof the game, 13 parameters) — the delivery resolves the target, stores the request and makes the tooltip visible; (c)AddButton/AddTabcreate the registration and ask the native host to rebuild (RefreshSkillBar/UpdateSkillTreeItems/RefreshSelected), but the visual creation of the element from a native prefab is not done here; (d)SetText/SetIconcoverUnityEngine.UI.Text/Image— TextMeshPro (which the game uses a lot) is outside this delivery and fails closed withMissingComponentException; (e) obtaining each host live (where the game keepsCurrentCharacterUI/MenuTabManager/SkillTreeManagerin the scene) is up to the consumer throughUiHosts— the API does not guess that path. - Publication to Thunderstore: a separate act, explicitly authorized; it was not done.
License and limits
MIT (api/LICENSE), author Gumatos. It does not include and does not redistribute game or third-party assemblies.
Português (BR)
Esta seção é o resumo curto em português; a página do pacote (o resto deste README e o CHANGELOG) está em inglês, na mesma forma das outras páginas deste projeto. A documentação interna em
docs/continua em português.
O que é. API de modding para Stolen Realm: uma base para outros mods, com helpers de interface e hooks observacionais opcionais. Ela não adiciona conteúdo jogável sozinha.
O que ela NÃO faz. Não implementa os domínios do inventário fora dos quatro hooks e da família UI
(combate detalhado, inimigos, quests, dados persistentes); não cobre TextMeshPro nem o preenchimento completo
de tooltip; e não descobre sozinha onde o jogo guarda cada hospedeiro de UI (isso é do consumidor, via UiHosts).
Versão alvo do jogo (fail-closed). O gate de runtime (ApiIdentity.Matches) compara apenas a versão
(Application.version = 1.3.1) e o SHA-256 do Assembly-CSharp.dll
(8bb4a7c512da20770a81dfac0347e9a68a8c0eff736f4566c52a66923b37855a). AppID 1330000, Build Steam 25240684 e
Unity 2022.3.62f2 são identidade documentada do alvo, não comparadas em runtime. Se versão ou hash
divergirem, a API não instala hook nenhum.
Instalar (previsto para depois da liberação). Instale só quando um mod que você usa pedir esta dependência e
houver versão liberada. Requisito declarado: BepInEx-BepInExPack-5.4.2305. O pacote completo põe os três
assemblies juntos em BepInEx/plugins/RealmApi/: RealmApi.dll, RealmApi.Core.dll e RealmApi.Hooks.dll.
Não copie apenas o RealmApi.dll e não deixe cópias antigas em subpastas de plugins (o BepInEx procura
recursivamente).
Desinstalar. Desinstale pelo gerenciador e revise os mods que dependem da API; na instalação manual, retire
somente a pasta RealmApi.
Configuração. BepInEx/config/com.gumatos.realmapi.cfg, seção Safety: EnableHooks = false por padrão.
Com true, o plugin ainda exige a versão e o hash exatos do alvo; toda divergência vira LogError e zero
hooks. Esse portão controla os hooks do plugin, não a família UI inteira.
Compilar. .NET SDK 6.0 (6.0.428) e as DLLs de referência em lib/; os cinco dotnet build -c Release de
api/ estão listados acima. Nada é instalado por padrão: só -p:DeployToBepInEx=true copia os três
assemblies para o perfil do r2modman.
Não exercitado nesta entrega. Carregamento pelo BepInEx no processo do jogo, gameplay, saves, multiplayer,
identidade em runtime, instalação no perfil r2modman e a família UI em runtime — tudo declarado
NAO_EXERCITADO com motivo na seção acima. Nenhuma publicação foi feita.