Lifecycle: load once, update while active
The current loader calls Load after the mod is registered and its declared client/server flag passes the loader’s mode check. It then checks the mod’s active config value. A false-to-true transition calls Activate; while the config stays true, the loader calls Update repeatedly. A true-to-false transition calls Deactivate.
| Method | Use it for | Design notes |
|---|---|---|
GetMetaData() | Stable identity and declared support | Return consistent name, version, author, target API, and mode flags. |
Load(context) | One-time preparation | Validate signatures/layouts, allocate resources, or prepare a hook. Fail closed. |
Activate(context) | Enable the feature | Install/enable prepared behavior only after validation succeeds. |
Update(context) | Poll settings or feature state | Runs repeatedly while enabled. Keep it cheap; cache unchanged settings. |
Deactivate(context) | Turn behavior off | Restore patched bytes, unregister callbacks, release active state. |
Unload(context) | Free owned resources | Be safe after partial load or failed activation. Avoid double-free and stale hooks. |
Unload is an interface requirement, but don’t assume it runs for every process exit or failure path. Keep destructors and cleanup idempotent.ModMetaData
Metadata tells the loader and users how the DLL describes itself. Current source uses client/server flags to skip loading when the running mode is not declared. Flags are declarations, not test evidence.
struct ModMetaData {
std::string name;
std::string description;
std::string version;
std::string author;
std::string targetShroudtopiaVersion;
bool hasClientSupport;
bool hasServerSupport;
};targetShroudtopiaVersion is descriptive in the current source path inspected; do not assume it is enforced as a compatibility gate. Test your mod against each loader version you claim to support.
ModContext API
Shroudtopia passes a pointer to one shared context object to lifecycle methods. The header exposes these facilities in SDK/API 0.1.1.
context->shroudtopia
version, config_file, log_file, and mod_folder identify framework version and common paths. Treat paths as runtime values; do not hard-code a user's install location.
context->game
version, isClient, and isServer describe the running environment. In this release source, version is initialized to the literal "SVN HERE"; it is a placeholder. The mode booleans are based on the executable-name heuristic.
context->config
Typed getters read one value from the named mod section and accept a fallback. Available scalar and array pairs:
GetBool / GetBoolArrayGetString / GetStringArrayGetInt / GetIntArrayGetFloat / GetFloatArrayGetUint64 / GetUint64Array
Each getter takes (modKey, key, defaultValue). Use your metadata name for modKey. Validate ranges and handle malformed or out-of-range values in your mod.
context->Log(const char*)
Write a message to the framework log. Prefix messages with your mod ID. Pass a stable null-terminated string, and avoid format characters if a consumer might interpret them.
registeredMods and loadedMods
The context exposes a map of registered mod objects and a set of loaded mod pointers. This is internal-style shared state: check lifetime and nullability, and avoid assuming iteration order or a cross-version ABI guarantee.
Configuration patterns
Read through the type that matches the JSON value. Supply a safe default and validate it before use. The loader’s update loop makes periodic reads possible; it does not make expensive work safe to repeat.
const int toggleKey = context->config.GetInt(
"example_mod", "toggle_key", 117); // F6 virtual-key code
const float distance = context->config.GetFloat(
"example_mod", "camera_distance", -0.15f);
const std::vector<uint64_t> ids = context->config.GetUint64Array(
"example_mod", "steam_ids", {});
if (toggleKey < 0 || toggleKey > 255) {
context->Log("[example_mod] invalid toggle_key; using default");
}For live-adjustable settings, compare the newly read value with the last applied value, then update only the associated data safely. Avoid rewriting executable instructions every poll.
Config safety
- Use explicit units and bounds in documentation.
- Define behavior for missing, wrong-type, non-finite, and out-of-range values.
- Do not mutate the config file from every
Updatecall. - Preserve user config across framework upgrades.
- Keep settings keys stable; if a rename is required, migrate old keys deliberately.
Memory utilities and sharp edges
context->memory exposes process-memory helpers. They are low-level mechanisms, not game APIs. A successful helper call cannot prove that a signature still identifies the intended instruction.
| Function | Current header signature | Purpose |
|---|---|---|
FindPattern | uintptr_t(const char*, const char*, uintptr_t, size_t) | Scan a bounded address range for a byte pattern and mask. |
Read / Write | bool(uintptr_t, buffer, size_t) | Copy bytes to or from an address. |
WriteToReadOnlyMemory | bool(LPVOID, LPVOID, SIZE_T) | Write through protected-memory handling. |
WriteJump | void(LPVOID target, LPVOID destination) | Write a jump at a target address. |
AllocateMemoryNearAddress | LPVOID(LPVOID, SIZE_T) | Request nearby memory for relative branches. |
GetModuleBaseAddress | uintptr_t(const std::string&) | Look up a loaded module base. |
ApplyPatch | bool(uintptr_t, uint8_t*, size_t) | Apply byte patch data. |
InjectShellcode | bool(uint8_t*, size_t, uintptr_t, uintptr_t*) | Inject bytes near a target and optionally return its address. |
CreateDetour | bool(LPVOID target, LPVOID detour, void** trampoline) | Declared detour helper; see release limitation below. |
CreateDetour in 0.2.1-shroudweave.1. Its current implementation returns true while the actual installation code is commented out. That return value does not mean a hook exists.Safer patch design
- Verify executable identity, image bounds, unique signature, expected original instructions, and all required data layouts.
- Reject zero, multiple, malformed, or already modified matches. Never patch on a partial match.
- Keep a copy of original bytes. Restore them on deactivation and every failure path.
- Allocate and free trampolines deterministically. Flush the instruction cache when required.
- Use synchronized access for values changed while game code reads them.
- Test unsupported versions and missing signatures; expected result is a safe inactive feature.
- Never treat a static scan, unit test, or successful build as proof of live gameplay behavior.