Every operation is one entry of the ops list of a mod manifest: a JSON object with "op"
and the parameters below. python tools/modkit/modkit.py ops lists the operations your copy of the kit provides.
Rules that hold for every operation:
- Paths (
source, gltf, image, ...) are relative to the mod folder, for example assets/ramp.glb. An absolute
path, or one that leaves the folder, is an error.
- Unknown parameters. The
static, texture and skin operations reject a parameter they do not know and list
the ones they accept, so a typo cannot silently do nothing. The collision and nodes operations ignore
parameters they do not know: check the spelling against this page (yaw_deg, rotation_y, ...). Keys that start
with _ or x_ are comments and are removed before the operation runs.
- Order matters. The operations of a build run in load order (packaging.md);
each one sees the files as the earlier ones left them. Collision operations that name a static object
(
"object": "modkit_kicker") must come after the static.add_object that made it.
- Failure writes nothing. The first error stops the build; the installed pack stays as it was.
- Coordinates are game world space: Y up, right-handed, meters.
rotation_y and yaw_deg are degrees about +Y; a
yaw of 0 leaves local +Z pointing at world +Z, and a positive yaw turns local +Z towards +X (local +Z becomes
(sin yaw, 0, cos yaw)).
- Budgets are checked after the operation and recorded in the build report (limits.md).
All the objects of one mod are named by you. The game only knows names by their checksum, so choose a prefix of
your own (modkit_, mymod_) and keep it unique within the zone.
Contents: static: add_object, replace_object,
replace_mdl, remove_object | texture:
replace, recolor | skin: common parameters,
replace_mesh, replace_part, add_mesh,
recolor, replace_texture, deck_graphic,
griptape, cas_texture | collision: add_mesh,
add_box, add_ramp, remove, copy |
rails: add, remove |
nodes: add_rail, add_gameobject | goals
How the operations are checked: the build runs, every rebuilt file parses back, the example mods render correctly in
the kit's own renderer, and each area runs in the native port; the overview lists what works in
game and the known limitations.
Geometry of zones (levels) and props. glTF in; the game's .geom / .mdl geometry, the material and texture
dictionary entries it needs, and the zone's node entry out. Library: tools/models/import_static.py.
How an import turns glTF into game data: each glTF primitive (one per material) becomes one game mesh, split
automatically when it exceeds 32,767 vertices or 65,535 strip indices. The vertex format is the template mesh's
(position, packed normal, tangents, color, half-float UVs). A glTF material gets a game material cloned from a
template of the same kind (opaque, cut-out, blended), its own texture is added to the zone's texture dictionary, and a
neutral normal map fills any normal-map slot. See blender.md.
Parameters shared by static.add_object, static.replace_object and static.replace_mdl:
Adds a new object to a zone: a ramp, a rail, a box.
If any of position, rotation_y, scale is given, the glTF node's own transform is ignored and the node's mesh (in
its local space) is placed by them. Without them the glTF world transform is the placement, as an object's position in
Blender is. The operation records the placed object (world triangles, bounding box) for the collision operations that
follow it.
{"op": "static.add_object", "zone": "z_houses", "name": "modkit_kicker", "source": "assets/ramp.glb",
"node": "modkit_kicker", "position": [-61.05, 2.39, -64.52], "rotation_y": 45, "snap_to_ground": true}
Game models. With game_model the kit exports the model from your game files to a temporary glTF at build time
(base-color textures embedded, the material references kept) and imports it like any other source; the file never
enters the mod folder. A prop needs position (its model space is around its own pivot, usually the bottom center). A
zone object without position / rotation_y / scale is duplicated in place, at its original spot. Materials the
target zone already has (a zone object copied within its own zone) are reused as they are; others (every prop
material) become clones of a zone material of the same kind with the model's own base-color texture, which adds that
texture to the zone (the budgets show the growth). Collision is not copied: add it with a collision.* operation.
{"op": "static.add_object", "zone": "z_houses", "name": "mymod_propane", "game_model": "GO_PropaneTank",
"position": [-61.0, 2.39, -64.0], "rotation_y": 30, "snap_to_ground": true},
{"op": "static.add_object", "zone": "z_houses", "name": "mymod_mailbox",
"game_model": "z_houses:Z_Houses_Street_JG_MailBox04", "position": [-62.5, 2.39, -63.0], "rotation_y": 90}
The object has no collision until a collision.* operation gives it some, and it exists only in the full zone, not
in the distant _lod stand-in (limits.md).
Swaps the meshes of an existing zone object for those of a glTF node. The object keeps its name, node and
collision; you can add or remove meshes inside it.
{"op": "static.replace_object", "zone": "z_houses", "object": "Z_Houses_Street_JG_FlowerRing01",
"source": "assets/ring.glb", "node": "modkit_ring", "lightmap": "object"}
Collision stays the old object's. If the shape changed, remove the old collision with collision.remove and add the
new with collision.add_mesh.
Replaces a prop (a .mdl model such as GO_PropaneTank) in every pak that holds it, so every place that prop
appears changes.
A prop is replaced as a whole: a model with several objects keeps only the first, the others lose their meshes. Lightmaps
are neutral (props are lit dynamically).
Hides a zone object: its meshes are removed; its record, node and collision stay.
Hides every object of a zone: a blank canvas for a park built from scratch (the level editor's "New park from
scratch"). Objects added earlier in the same build are kept, so put it after your static.add_object ops: the
importer clones the zone's own materials as templates, which a cleared zone no longer offers.
Hiding the largest zone's geometry also shrinks its _vram pak, which frees video memory for every level
(limits.md). Pair it with collision.clear and rails.remove all, or the skater
still hits and grinds the invisible level.
Any texture of the game, by dictionary entry. A texture is found by its checksum (8 hex digits, the form the kit's
exports use in their image names) or its name. Library: tools/tex/texdict.py.
Puts your picture in place of a game texture. The size may change (the dictionary entry, the mip chain and the pixel
data are rebuilt together).
Sizes that are not powers of two are resized to the nearest power of two (at most 2048); the mip chain is generated;
cube and volume textures are not supported. The operation changes every container of the pak that holds the texture and
fails if the texture is not there.
{"op": "texture.replace", "pak": "z_houses", "texture": "794d6ffd", "image": "assets/drywall.png"}
A build-time color edit of a game texture, so a mod ships parameters instead of game pixels. The texture is
decoded from the player's own game files, changed, and encoded again.
At least one of hsv, tint, colormap is required. The steps run in the order colormap, hsv, tint; alpha is kept.
{"op": "texture.recolor", "pak": "z_houses", "texture": "794d6ffd", "hsv": [200, 1.0, 1.0]}
For skater textures, which are shared atlases, use skin.recolor: it finds the texture through the
model and changes only the texels one mesh uses.
Paints over a game texture with a layer of your own (graffiti, a logo, scuffs): the mod ships only the layer, a
PNG with transparency; the build composites it onto the player's copy of the texture.
{"op": "texture.overlay", "pak": "z_houses", "texture": "794d6ffd", "image": "assets/tag.png", "opacity": 0.9}
The level editor's texture tools write this op ("Paint over...").
Skinned models: skaters, create-a-skater (CAS) clothing and accessories, boards, deck graphics. Library:
tools/models/import_skin.py. Units and axes are the kit's export space: game model space, Y up, meters, the skater
faces +Z, the board's nose is +Z.
Free skate, skater select and create-a-skater draw a pro from the cas_shared pak, not from his standalone files, so
the default is right for most mods.
Replaces game meshes with glTF meshes, normally ones exported by the kit and edited in Blender
(modkit.py export skater pro_hawk, blender.md).
Bones are matched by name (case-insensitive; bone_<crc8> for unnamed bones); weighted joints the game skeleton does
not have fail the import with the list. At most 3 weights per vertex are kept (the largest, renormalized). Meshes whose
vertices, normals, weights and triangles did not change keep their bytes. A changed mesh is rebuilt; if it is over the
SPU skinning budget it is split automatically unless allow_ppu.
{"op": "skin.replace_mesh", "skin": "Pro_Hawk", "gltf": "assets/hawk_head.glb", "node": "Pro_Hawk_head"}
Same parameters as replace_mesh, plus hide_missing (default true), template (a mesh index: descriptor donor for
new meshes) and material. The glTF is the whole model: primitives that map to a game mesh replace it, the others
are added as new meshes, and game meshes that are not in the glTF are hidden (one degenerate triangle) unless
hide_missing is false. A whole-character replacement on Tony Hawk's slot works.
Adds a mesh to a skinned model.
{"op": "skin.add_mesh", "skin": "Pro_Hawk", "gltf": "assets/cap.glb", "node": "modkit_cap",
"rigid_bone": "Bone_Head", "offset": [0, 0.03, -0.012], "scale": 1.1}
The new mesh draws after the mesh it copies its descriptor from. It adds a material and a texture of its own to the
model's texture dictionary.
Recolors the texture(s) of a skinned model at build time.
{"op": "skin.recolor", "skin": "Pro_Hawk", "mesh": 4, "hue": 150, "saturation": 1.9, "value": 1.35}
Puts your picture on a skinned model's texture.
Replaces a skater's deck graphic. A pro's deck graphic is a texture swap of the CAS system (the Board's default deck
texture is replaced by Decks_X); this edits that swap image.
The deck graphic is a portrait picture, u across the deck and v along it. One of skater or deck is required.
Tony Hawk's graphic is the 128 x 256 DXT1 image Decks_Birdhouse_01; the size is kept unless you give size.
{"op": "skin.deck_graphic", "skater": "Hawk", "image": "assets/deck.png"}
The same for the griptape slot: image, skater or desc (Generic 1, Grip_Zombie, Grip_VooDoo), fit,
rotate, size.
Any CAS texture-swap list: slot (e.g. deck_graphic, griptape) plus desc or skater, image, fit, rotate,
size. deck_graphic and griptape are shortcuts for it.
New solid surfaces in a zone's collision (Havok .hkc). The visual geometry of an object and its collision are
separate data, so these operations add collision only: pair them with static.add_object to see what you hit. See
collision-rails.md for what the game does with them and what is not supported.
Common parameters:
Limits per operation: 65,535 triangles and 65,535 vertices; 254 distinct material words.
A triangle mesh as collision.
{"op": "collision.add_mesh", "zone": "z_houses", "object": "modkit_kicker"}
A box.
{"op": "collision.add_box", "zone": "z_houses", "object": "modkit_rail", "yaw_deg": 45}
A parametric kicker, without needing a mesh.
Takes the collision bodies of a node (stock or added) out of the zone: zone, name. Use it after
static.replace_object changed an object's shape, then add the new shape.
Takes every collision body of the zone out: zone, and keep (node names, or prefixes ending in *). Bodies
added earlier in the same build are kept: put it after your collision.add_* ops. (rails.remove all, in
contrast, goes before your rails.add ops.)
Copies the collision of an existing node as a new body: zone, from (the node, e.g. Z_Houses_g_nuclear_kicker),
name, offset [dx, dy, dz], rotation_y (degrees about pivot, default the source's base center), pivot.
Boxes and cylinders of the source become convex hulls. Useful to clone a stock ramp where you want one.
Grind rails the game really builds. In the retail game (script global RAILNODE_ASSETS_ENABLED = 1) the rails of a
level come from pak/global_rails/global_rails.pak: one baked .rnb_lvl file per zone, loaded once per level. The
zone's node array also lists every rail point as a RailNode node, but the game only reads those by name (scripts) and
in the park editor. rails.add writes both, like the game's own tools did. Format, loader and limits:
collision-rails.md; library tools/models/thp8rnb.py. Seen working in the game: the
skater grinds a rails.add rail, and a rail only in the node array is not built
(what is verified).
Adds a grind rail: a polyline the skater can snap to and grind along. Rails are not collision: a rail has no
surface of its own, so give it a visual (static.add_object) and, if it must be solid, a collision body.
The rail line is one of:
Other parameters:
{"op": "rails.add", "zone": "z_houses", "name": "p8rail_line", "cluster": "p8rail", "terrain": "MetalPole",
"points": [[-54.3335, 0.539, -57.806], [-50.798, 0.539, -54.2704]]}
A rail needs at least two points; no segment may be shorter than 5 cm. One operation per rail: a level editor turns
each drawn or generated rail (coping, ledge edge, funbox rail) into one rails.add with its world points.
Budget: the rails of all zones share RailNodeHeap (1,761,280 bytes; the stock global_rails.pak uses 1,742,848 of
them once padded), so about 150 rail nodes fit before the build raises the heap: rails.add then sets the heap size in
the startup script in 64 KiB steps (a warning; at most 1 MiB over the stock size, taken from main memory). The build
report records RailNodeHeap (global_rails.pak).
Removes rails from a zone's baked rails (the records the game grinds on). zone and one or more of:
The points' neighbors are unlinked, so what is left of a cut rail ends there. The nodes stay in the node array
(scripts may name them; the game builds no rail from them). Order matters as everywhere: all removes the rails added
by earlier operations unless keep names them, and does not touch rails added after it.
{"op": "rails.remove", "zone": "z_houses", "name": "Z_Houses_NxSpline_76_KNOT_0"}
{"op": "rails.remove", "zone": "z_houses", "all": true, "keep": ["p8rail_line"]}
{"op": "rails.remove", "zone": "z_houses", "within": {"center": [-60, 3, -63], "radius": 6}}
Entries of the zone's node array (the .nqb): the data that scripts and the engine read to place and wire things.
The same operation as rails.add, with the same parameters (kept so existing mods and editor projects
build unchanged; new mods should write rails.add).
Places another copy of a game object that the zone already has (a barrel, a bin) by cloning its node.
The object's model must already be loaded in that zone: you place more of what the zone has, not new models (use
static.add_object for a new model).
Moves or turns an existing node: a restart point, a game object, a trigger.
{"op": "nodes.set", "zone": "z_houses", "node": "Z_Houses_TRG_Restart_Default", "pos": [-60, 2.39, -60], "yaw_deg": 90}
The free-skate start of a level is its ..._TRG_Restart_Default node, so this moves where free skate begins.
Adds a restart point (a copy of one of the zone's restart nodes).
People: peds.add (kinds stand, walker, skater, goal_giver), peds.remove, peds.replace_appearance,
peds.ambient. The full reference, with what the game does underneath, is peds.md.
{"op": "peds.add", "zone": "z_houses", "name": "mymod_suit", "pos": [-60.13, 2.39, -65.16], "yaw_deg": 225,
"profile": "RPed_Businessmen", "anim": "PedM_ArmCrosIdle01_xx"}
Custom goals (quests) for the free-skate world: goals.add_score, goals.add_combo, goals.add_collect,
goals.add_race, goals.add_gap, goals.add_trick, and goals.debug for test builds. They have their own page,
goals.md: every parameter, how goals start (marker, person, pause menu, script), chains, the intro
presentation, the runtime scripts and results for Lua and stories, and how the game's goal system works.
{"op": "goals.add_collect", "name": "myname_street_tokens", "title": "Street Tokens", "text": "Grab all 5 tokens!",
"pickups": [[-54.68, 1.1, -58.15], [-52.56, 1.1, -56.03], [-50.44, 1.1, -53.91]], "model": "stoken", "time": 60,
"start": {"trigger": "marker", "pos": [-57.51, 0.19, -55.33], "yaw": 45, "model": "chalk"}}
Custom soundtrack: music.add, music.folder, music.replace, music.disable, music.options. The full
reference, with how the game plays music, is soundtrack.md.
{"op": "music.add", "file": "assets/my_song.ogg", "artist": "Me", "title": "My Song", "genre": "Punk"}