Mods (CMOD plugins)¶
Crimsonland has a lightweight plugin system that loads DLLs from the mods\ folder
and expects two exports: CMOD_GetInfo and CMOD_GetMod.
Discovery¶
- The game checks for any mod DLLs via
mods_any_available(0x0040e940). - It searches the filesystem with
mods\*.dll(FindFirstFileA/FindNextFileA). - In
game_state_set(0x004461c0), this boolean gates whether the mods entry is enabled in the main menu flow.
Loading a mod info block¶
mod_load_info (0x0040e700):
- Builds a path
mods\%sand callsLoadLibraryA. - Resolves
CMOD_GetInfoviaGetProcAddress. - Calls it and copies the returned struct into
DAT_00481c88(0x12 dwords). - Logs either the info or an error string to the console.
Mod info layout (CMOD_GetInfo)¶
Both bundled mods (cl_nullmod.dll, cl_crimsonroks.dll) return a 0x48-byte
struct with two fixed-size strings and two scalar fields. The executable also
seeds the tail values (1.0f, 3) before copying.
| Offset | Field | Meaning | Evidence |
|---|---|---|---|
0x00 |
name[0x20] |
Mod display name (NUL-terminated) | Mod DLLs zero 0x20 bytes then copy "Null Example Mod" / "CrimsonRoks" into the base. |
0x20 |
author[0x20] |
Author string (NUL-terminated) | Mod DLLs copy "temper / 10tons" into the second 0x20 block. |
0x40 |
version (float) |
Mod version | cl_nullmod writes 1.0f; cl_crimsonroks writes 0.2f. |
0x44 |
usesApiVersion (u32) |
Mod API/version marker | Both mods write 3 (CL_API_VERSION in cl_mod_sdk_v1/ClMod.h); the exe seeds DAT_00481ccc = 3 during init. |
Loading a mod interface¶
mod_load_mod (0x0040e860):
- Builds a path
mods\%sand callsLoadLibraryA. - Resolves
CMOD_GetModviaGetProcAddress. -
Calls it and (on success) writes a context pointer at offset
+4of the returned interface. -
Logs success/failure to the console.
Mod interface object (CMOD_GetMod)¶
The mod interface object is a 0x408-byte allocation with a vtable pointer at
offset 0x00. This matches modExport_t from cl_mod_sdk_v1/ClMod.h (C++
interface + a 0x400-byte parms block).
| Offset | Field | Meaning | Evidence |
|---|---|---|---|
0x00 |
vtable |
Function table (3 slots used) | Exe calls (*vtable)[0], (*vtable)[1], (*vtable)[2](frame_dt_ms). |
0x04 |
cl |
Mod API context pointer | Exe writes &DAT_00481a80 at +4 after CMOD_GetMod. |
0x08 |
parms.drawMouseCursor |
Draw the standard cursor | Mods set this to 1; exe checks (char)plugin_interface_ptr[2] to decide whether to draw the cursor. |
0x09 |
parms.onPause |
Pause hint from the engine | cl_crimsonroks gates updates on parms.onPause; exe sets *(plugin_interface_ptr + 9) = 1 when pausing. |
0x24 |
parms.request_exit |
Exit/request flag byte | Exe sets byte +0x24 when leaving or pausing the plugin flow (part of the reserved parms block). |
Mod interface vtable (3 slots)¶
The bundled mods use a 3-entry vtable. The update slot returns a byte that drives whether the exe keeps the mod active.
| Slot | Meaning | Evidence |
|---|---|---|
0 |
Init() |
Both mods cache the context pointer and set parms.drawMouseCursor (byte +0x08) to 1. |
1 |
Shutdown() |
Both mods call internal cleanup helpers and delete this. |
2 |
Frame(frame_dt_ms) |
Returns 0 to exit (exe closes the plugin). Used to poll keys and issue "game_pause". |
Mod API context (DAT_00481a80)¶
The context pointer passed at +0x04 is treated as a vtable-based API from
within the mod DLLs. The layout matches clAPI_t in cl_mod_sdk_v1/ClMod.h
(API v3), and the vtable pointer is set to 0x0046f3e4 during init.
| Vtable offset | SDK name | Wrapper (crimsonland.exe) | Notes |
|---|---|---|---|
0x00 |
CORE_Printf |
mod_api_core_printf (0x0040e000) |
Uses OutputDebugStringA. |
0x04 |
CORE_GetVar |
mod_api_core_get_var (0x0040e040) |
Returns a 3-pointer var_t view (id, stringValue, floatValue). |
0x08 |
CORE_DelVar |
mod_api_core_del_var (0x0040e080) |
Unregisters a cvar. |
0x0c |
CORE_Execute |
mod_api_core_execute (0x0040e0a0) |
Executes a console line. |
0x10 |
CORE_AddCommand |
mod_api_core_add_command (0x0040e0c0) |
Registers a console command. |
0x14 |
CORE_DelCommand |
mod_api_core_del_command (0x0040e0e0) |
Unregisters a command. |
0x18 |
CORE_GetExtension |
mod_api_core_get_extension (0x0040e100) |
Handles "grimgfx", "grimsfx", "IDirect3D8". |
0x1c |
GFX_Clear |
mod_api_gfx_clear (0x0040e1f0) |
Bridges to grim_clear_color. |
0x20 |
GFX_GetStringWidth |
mod_api_gfx_get_string_width (0x0040e220) |
Bridges to grim_measure_text_width. |
0x24 |
GFX_Printf |
mod_api_gfx_printf (0x0040e240) |
Preformats into a global buffer, then draws text. |
0x28 |
GFX_LoadTexture |
mod_api_gfx_load_texture (0x0040e280) |
Loads mods\\%s via texture_get_or_load("CLM_%s", ...). |
0x2c |
GFX_FreeTexture |
mod_api_gfx_free_texture (0x0040e2e0) |
Bridges to grim_destroy_texture. |
0x30 |
GFX_SetTexture |
mod_api_gfx_set_texture (0x0040e300) |
Bridges to grim_bind_texture(handle, 0). |
0x34 |
GFX_SetTextureFilter |
mod_api_gfx_set_texture_filter (0x0040e380) |
Bridges to grim_set_config_var(21, filter). |
0x38 |
GFX_SetBlendMode |
mod_api_gfx_set_blend_mode (0x0040e3a0) |
Bridges to grim_set_config_var(19, src) / (20, dst). |
0x3c |
GFX_SetColor |
mod_api_gfx_set_color (0x0040e320) |
Bridges to grim_set_color. |
0x40 |
GFX_SetSubset |
mod_api_gfx_set_subset (0x0040e350) |
Bridges to grim_set_uv. |
0x44 |
GFX_Begin |
mod_api_gfx_begin (0x0040e3e0) |
Bridges to grim_begin_batch. |
0x48 |
GFX_End |
mod_api_gfx_end (0x0040e400) |
Bridges to grim_end_batch. |
0x4c |
GFX_Quad |
mod_api_gfx_quad (0x0040e420) |
Bridges to grim_draw_quad. |
0x50 |
GFX_QuadRot |
mod_api_gfx_quad_rot (0x0040e470) |
Sets rotation then draws a quad. |
0x54 |
GFX_DrawQuads |
mod_api_gfx_draw_quads (0x0040e4c0) |
Submits vertex data (offset 0,0). |
0x58 |
SFX_LoadSample |
mod_api_sfx_load_sample (0x0040e530) |
Loads mods\\%s via sfx_load_sample. |
0x5c |
SFX_FreeSample |
mod_api_sfx_free_sample (0x0040e560) |
Releases a sample. |
0x60 |
SFX_PlaySample |
mod_api_sfx_play_sample (0x0040e570) |
pan is scaled by 512.0. |
0x64 |
SFX_LoadTune |
mod_api_sfx_load_tune (0x0040e5b0) |
Loads mods\\%s via music_load_track. |
0x68 |
SFX_FreeTune |
mod_api_sfx_free_tune (0x0040e5e0) |
Releases a track handle. |
0x6c |
SFX_PlayTune |
mod_api_sfx_play_tune (0x0040e5f0) |
Wrapper name suggests SFX; likely “exclusive” tune play. |
0x70 |
SFX_StopTune |
mod_api_sfx_stop_tune (0x0040e600) |
Wrapper name suggests SFX; likely tune stop/mute. |
0x74 |
INP_KeyDown |
mod_api_inp_key_down (0x0040e660) |
Uses grim_is_key_active (key 1 forced false). |
0x78 |
INP_GetAnalog |
mod_api_inp_get_analog (0x0040e620) |
Special-cases DIKA_MOUSEXSTAT/DIKA_MOUSEYSTAT (355/356). |
0x7c |
INP_GetPressedChar |
mod_api_inp_get_pressed_char (0x0040e610) |
Bridges to grim_get_key_char. |
0x80 |
INP_GetKeyName |
mod_api_inp_get_key_name (0x0040e680) |
Bridges to input_key_name. |
0x84 |
CL_EnterMenu |
mod_api_cl_enter_menu (0x0040e690) |
Only handles "game_pause". |
Open questions¶
- Remaining semantics of the reserved
parmsbytes beyonddrawMouseCursor,onPause, and the observedrequest_exitflag. - Better naming for the tune wrappers (
SFX_PlayTune/SFX_StopTune) if they differ from the core SFX system behavior.