rsmm.sdk.builder
rsmm.sdk.builder
Section titled “rsmm.sdk.builder”Mod-builder used by from rsmm import sdk; with sdk.Mod(...) as m:.
Collects config/i18n/content/dependency declarations in memory, then
materializes the whole mod on __exit__. One transactional write per
mod build avoids half-built mod trees on the disk.
Mod.asset
Section titled “Mod.asset”Mod.asset(self, decoded_path: 'str', source: 'str | Path') -> 'None'Stage a source asset to override the game file at decoded_path.
decoded_path is the plaintext asset path as it appears in
data/asset_map.json (forward slashes), e.g.
3D/Characters/Heroes/Melusine/Textures/T_Melusine_ALB.png.
The file is copied verbatim into mods/<id>/assets/<decoded_path>;
rsmm apply auto-cooks source formats (.glb/.png/.dds/…) into the
cooked container and installs them. Pre-cooked inputs pass through.
Mod.boss
Section titled “Mod.boss”Mod.boss(self, id: 'str', *, base: 'str', becomes: 'str | None' = None, **fields)Make the arena of boss base spawn boss becomes instead.
Mod.enemy
Section titled “Mod.enemy”Mod.enemy(self, id: 'str', *, base: 'str', name: 'str | None' = None, **fields)Register a custom enemy cloned from vanilla base.
Mod.hero
Section titled “Mod.hero”Mod.hero(self, id: 'str', *, base: 'str', name: 'str | None' = None, abilities: 'list | None' = None, **fields)Register a custom hero cloned from vanilla base.
Mod.item
Section titled “Mod.item”Mod.item(self, id: 'str', *, base: 'str', name: 'str | None' = None, **fields)Register a custom item cloned from vanilla base.
Mod.map
Section titled “Mod.map”Mod.map(self, id: 'str', *, base: 'str', **fields)Register a custom map/level cloned from vanilla base.
Mod.model
Section titled “Mod.model”Mod.model(self, decoded_path: 'str', source: 'str | Path', rotate_deg: 'tuple[float, float, float] | None' = None, scale: 'float | None' = None, skin: 'str | None' = None, fit: 'str | None' = None, submeshes: 'str | None' = None, bones: 'dict[str, str] | None' = None, drop_bones: 'list[str] | None' = None) -> 'None'Override a mesh asset. Source must be a .glb/.gltf.
A custom mesh is cooked at apply-time by retargeting it onto the
game asset’s skeleton (engine.geometry_cook). It is auto-scaled and
recentered into the original’s space; orientation defaults to an
auto-upright guess (tallest axis -> up). If the mesh comes out turned
or upside down, pass rotate_deg=(x, y, z) — a rigid rotation in
degrees applied before the fit (e.g. (90, 0, 0) to flip upright,
(0, 180, 0) to face the other way).
The auto-fit matches only the tallest axis, so a mesh with a
different aspect ratio than the original can come out too big or
small. Pass scale= to multiply the auto-fit (e.g. scale=0.5 to
halve it).
Replacing a SKINNED mesh — a character, not a prop — has one rule the fit cannot do for you: author your mesh in the original’s bind pose. The cooker has no bones of yours to use; it keeps the game’s skeleton and gives each of your vertices the weights of the game vertices nearest to it. That copy is positional, so an arm vertex that lands nearest a leg bone gets weighted to the leg, and the model tears itself apart the moment it animates. Line the limbs, head and torso up with the original in Blender before exporting.
skin="rigid" binds the whole mesh to a single bone instead: it
follows the character but never deforms. Right for a prop or a static
shape carried by a character, wrong for a body.
skin="gltf" uses the weights in your OWN .glb — the way to replace
a character properly. Rig the mesh to the game’s skeleton in Blender
(the bone names have to match the original’s) and export with the
armature; bones are then bound by name and nothing is guessed from
proximity, so the bind-pose rule above stops applying. It implies
fit="rig" (no scale, no recentre), because a mesh rigged to the
game’s skeleton is already in the right place. bones={"mine": "theirs"}
renames on the way in, for a rig that came from elsewhere.
drop_bones=[...] deletes the geometry a bone drives — for a donor
body carrying something that has to become a separate object, like a
chain or a slung weapon. Needs skin="gltf", since only your own
weights say which bone owns which triangle.
fit="none" keeps the mesh’s own size rather than matching the
original’s height — what a structure usually wants, since its donor is
a mounting point rather than a size reference. fit="rig" goes
further and does not move the mesh at all.
submeshes="map" lays your submeshes onto the original’s one for one
instead of merging them all into the first. A character is usually
several submeshes because each one draws with a different material, and
merging leaves the rest as empty stubs — so everything renders with the
first material and your second texture never appears. The pairing is by
order, so order your objects in Blender to match the original’s.
Mod.ot
Section titled “Mod.ot”Mod.ot(self, selector: 'str', field: 'str', value, *, file: 'str | None' = None, selector_field: 'str | None' = None) -> 'None'Change one field of a plaintext .ot the game ships uncooked.
selector picks the block by its m_sLabel (pass
selector_field for a different key), field is the field inside
that block, and value replaces what the file holds. The field must
already exist — an ot patch edits, it never invents a field the
engine would ignore.
file defaults to DarkTalesResources/ApplicationSettings.ot, the
one uncooked file with anything worth changing in it (entity-value
modifier descriptors, friendly-fire factors, the forced-seed option).
mod.ot("Merlin DMG Zone", "m_eComputerType", 0)The game’s own bytes are the base, so this ships three strings instead of a copy of a game file, and survives a patch that edits other parts of the same file.
Mod.skinpack
Section titled “Mod.skinpack”Mod.skinpack(self, name: 'str', key: 'int', *, ac_id: 'str' = '', al_id: 'str' = '', base_id: 'str' = '') -> 'None'Register a custom selectable skin-pack slot.
The skin roster is hardcoded in the engine (a fixed 9 oCAdditionalContent
“SkinPack” entries); see docs/_re/kinds/skins.md. A new slot can only be
added at runtime, so this is realized by the native loader DLL
(src/loader/src/hook_skins.cpp), which post-detours the roster builder and
appends a standalone entry per def. Defs are staged to
mods/<id>/skinpacks.json; the loader aggregates them across enabled mods.
Args:
name: display name shown in the skin grid.
key: pack key (int) the engine matches against a hero’s pack-id
field (entry +0x3c). Must be unique across packs.
ac_id: AC content id (entry +0x50), e.g. RW000PSAC000000A.
al_id: AL content id (entry +0x60), e.g. RW000PSAL000000A.
base_id: base content id (entry +0x70).
Note: registering the slot is verified, but the engine’s skin-grid
populate (FUN_1401f0f10) only renders a node as a button when the
manager filter (vtable[1]) returns 3. Two derived filter variants
exist: the Steam build checks ISteamApps::BIsDLCEnabled via the
node key (+0x3c); the local build checks a DLC entitlement file
path at node+0x80. A brand-new key is rejected by both gates, so
the slot is on the roster list but invisible by default. To prove the
button path in-game, build the loader and run with
RSMM_SKIN_FORCE_SHOW=1 — it force-pushes the node into the grid
and logs the filter verdict (see docs/_re/kinds/skins.md A1/A2).
The dlc_path field can be set in skinpacks.json for the local
filter variant. Asset resolution for a new slot is likewise unconfirmed;
stage the per-skin cooked assets with
:meth:asset/:meth:texture/:meth:model, and until the resolver
is mapped, prefer reusing an existing slot’s AC/AL id.
Mod.summary
Section titled “Mod.summary”Mod.summary(self) -> 'dict'Snapshot of everything staged so far — print before :meth:commit
to see exactly what the mod will write (no disk writes).
Mod.tag
Section titled “Mod.tag”Mod.tag(self, tag_id: 'str', members) -> 'None'Group content into a named, cross-mod-extensible tag (Minecraft
#namespace:path tags analog).
members is a ContentRef / id string, or an iterable of them.
Calling tag() again with the same id appends (de-duped,
order-preserved), so several mods — or several call sites — can grow
one tag. Members are deref’d to raw ids, so you can pass the handles
returned by :meth:item/:meth:enemy/… directly::
dagger = m.item("RubyDagger", base="Knife")m.tag("daggers", [dagger, "VanillaKnife"])Tags are written to mods/<id>/tags.json; downstream tooling/mods
aggregate them (same model as skinpacks.json).
Mod.texture
Section titled “Mod.texture”Mod.texture(self, decoded_path: 'str', source: 'str | Path') -> 'None'Override a texture asset. Source must be a .png/.dds/.tga.
Mod.validate
Section titled “Mod.validate”Mod.validate(self) -> 'list[str]'Return human-readable warnings about the current state (does not
raise). Empty list = clean. Called automatically by :meth:commit;
run it yourself for a fast pre-flight.
