Game skin contract v2
Read as Markdown · JSON schema · Per-game catalog
Build a custom game model from static GLB pieces and a versioned JSON skin. In v2, you can position, rotate and scale the body, each named physical control and the complete live display assembly. Add looping model animation, result-triggered clips and custom card, coin or Plinko motion. Betworld supplies game state, clickable actions and the final result.
Use game_skin_contract({"game":"blackjack"}) to get a v2 starter and the exact targets and motion channels for that game. Existing v1 skins continue to use their original fixed layout. The world document itself remains version: 1.
Use the current contract hash from the per-game catalog for a new skin. acceptedContracts lists earlier compatible hashes too. Existing blackjack and baccarat skins retain the card and discard positions associated with their hash when the native tables are updated. To adopt a newer layout, download its reference model, adjust your model to its anchors, and update the hash.
Manifest fields
Every object is strict: use only the fields in its schema. All numbers must be finite. UTF-8 JSON must fit 65,536 bytes for v2, or 16,384 bytes for v1.
| Field | Type and limit | Meaning |
|---|---|---|
version | 2 | Skin format. 1 remains supported without the three v2 arrays. |
id | ^[a-z][a-z0-9_-]{0,47}$ | Unique skin ID within your world |
game | One of the thirteen game asset IDs | Must match all assigned entities |
contract | Current 16-character lowercase hex fingerprint | Reference geometry/version returned by MCP |
body | 64-character lowercase hex hash | Converted body asset from your library |
bindings | 0–32 entries; required | Replacement visual meshes |
placements | 0–34 entries; defaults to [] | Presentation layout changes |
clips | 0–16 entries; defaults to [] | Model/control/display animation |
motions | 0–4 entries; defaults to [] | Result-object motion overrides |
A world may define up to 32 skins. Assign one with "skin":"my-blackjack" on an existing kind:"game" entity whose asset matches the skin. Entity position/yaw place the whole station; its entity scale stays [1,1,1] and floor Y stays 0. Props do not take skin references. To restore the native model, remove the entity's skin field.
Mesh bindings
Each binding has exactly two fields:
{"slot":"control.hit","asset":"HASH_RETURNED_BY_ASSET_UPLOAD"}
Use a control.<slot> from the selected game's controls catalog or a part.<slot> from its parts. Each slot may appear once. Omitted bindings retain their native visual; bindings: [] is valid. Multiplayer Blackjack controls retain their original seat identity. Video Poker Hold controls retain their card index. Doubler and Crash expose no physical-button bindings because their buttons are regions of the live display.
Upload the body and each distinct button/surface as separate static GLBs. One button hash can be reused across multiple slots. Uploaded names or extras do not select actions. Action/index/seat identity comes from the reference catalog, while the station comes from the assigned game entity. Bets use the currently authenticated player and the ordinary game endpoints.
Coordinate system and fitting
Positions use station-local world units, Y up, before the game entity's position/yaw. +Z faces the usual player side. Rotation uses XYZ Euler radians, with rotation matrix Rx × Ry × Rz. Scale is an axis multiplier. In Blender, game [x,y,z] becomes Blender (x,-z,y). Convert rotations by conjugating the matrix with that axis conversion; do not merely swap Euler components.
bodyBounds and each control's bounds already include the native source-to-world scale. localBounds is the native button mesh's geometry box. matrixSource is a column-major 4×4 source transform, with sourceToWorldScale supplied separately.
Before placement, the complete body GLB is fitted to bodyBounds. Each bound button/surface is fitted to the native mesh's localBounds and parented to its trusted proxy. The fit is axis by axis: scale = targetExtent / uploadedExtent, followed by center alignment. A zero-extent axis uses scale 1. Match the reference width:height:depth to avoid unintended stretching. Export only the intended piece; stray geometry changes its bounds.
Placements: moving controls and the live display
Each placement has exactly target, position, rotation and scale:
{
"target":"control.hit",
"position":[0.6,0,0],
"rotation":[0,0.15,0],
"scale":[1.2,1,1.2]
}
This offsets Hit by 0.6 world units along the station's X axis. position is an offset from the reference pose, not an absolute coordinate. Rotation and scale pivot around that control's reference bounds center. The body uses its own bounds center. The display pivots around the station origin.
| Target | What moves |
|---|---|
body | Your fitted body assembly |
control.<slot> | Native clickable proxy, custom/native visual and its raised label together |
display | Complete live presentation: screens, cards, grids, pegs, coin/result readouts, surface overlays and their generated click regions |
presentation.targets from the catalog is the exact target list. Part bindings such as part.playing_surface move with display; they are not independent placement targets. Physical control.* targets are independent of display, so place them explicitly when redesigning a panel. Multiplayer Blackjack's three generated Bet/Settings panels move with its display, while its twelve physical hand controls use their own targets.
A target may have one placement. Each position component is in [-4,4], each rotation in [-4π,4π], and each scale component in [0.5,2]. Defaults are identity when a target is omitted. Keep controls visible and within ordinary interaction reach. These transforms affect presentation and interaction proxies; station collision hulls and player/seat positions retain their reference shape. Build a shell that fits that walkable footprint. For the familiar table/player ratio, leave display Y offset at 0 and scale at 1.
Clips: idle and result animation
A clip has exactly target, trigger, durationMs and keyframes:
{
"target":"body",
"trigger":"result",
"durationMs":900,
"keyframes":[
{"time":0,"position":[0,0,0],"rotation":[0,0,0],"scale":[1,1,1]},
{"time":0.5,"position":[0,0.08,0],"rotation":[0,0,0],"scale":[1.02,1.02,1.02]},
{"time":1,"position":[0,0,0],"rotation":[0,0,0],"scale":[1,1,1]}
]
}
idle loops after the station loads. result plays once when a new accepted result ID arrives; for shared games it uses the settled round ID. Clips do not place a bet. A result clip temporarily overrides the idle clip on the same target. Idle resumes afterward. Starting the next active/busy bet clears the preceding result clip.
Duration must be an integer from 200 to 5,000 ms. A clip contains 2–32 keyframes. time is normalized 0–1, starts at 0, ends at 1 and increases strictly. Positions/rotations/scales have the placement bounds above. The first and last keyframe must be identity: position and rotation [0,0,0], scale [1,1,1]. Between frames, components interpolate linearly. Angles are not shortest-path quaternion interpolation; provide intermediate angles for deliberate spins.
Each (target,trigger) pair may occur once. Clip transforms are relative to the target's placement: the effective transform is placement × animatedOffset, both around the documented pivot. Result clips animate the assembly's presentation, not the values painted into it.
Result motion: cards, coins and balls
A motion has exactly channel and keyframes. Each frame has time, progress, position and rotation; it has no scale or duration.
{
"channel":"coin_flip",
"keyframes":[
{"time":0,"progress":0,"position":[0,0,0],"rotation":[0,0,0]},
{"time":0.45,"progress":0.3,"position":[0,0.6,0],"rotation":[0,0,1.4]},
{"time":0.8,"progress":0.85,"position":[0,0.2,0],"rotation":[0,0,0.5]},
{"time":1,"progress":1,"position":[0,0,0],"rotation":[0,0,0]}
]
}
Time uses the game's existing presentation duration: card_deal 340 ms per card, card_flip 260 ms per reveal, coin_flip 2,800 ms, and plinko_drop 2,160 ms. Read presentation.motionDurationsMs from the current contract for these values. Native sequencing and any reveal pauses still apply. progress retimes the authoritative path, remains within 0–1 and never decreases. It must start at 0 and end at 1. Position is a path offset in station-local world units, each component [-2,2]. Rotation is an additional XYZ rotation, each component [-8π,8π]. Both endpoint positions and rotations must be zero. Frames have the same 2–32 count and strictly increasing 0–1 times as clips. A channel may occur once.
| Game | Motion channels | Authoritative elements |
|---|---|---|
| Blackjack | card_deal, card_flip | Server hand ranks, deck/hand destination and dealer hole-card disclosure |
| Multiplayer Blackjack | card_deal, card_flip | Same, shared across all three original seats |
| Baccarat | card_deal | Player/Banker ranks and server-selected third cards |
| Heads or Tails | coin_flip | Final Heads/Tails orientation and result readout |
| Plinko | plinko_drop | Server path, risk and destination pocket |
| Mines, Keno, Drago Tower, Dice, Limbo, Video Poker, Doubler, Crash | Use display clips | Live grid, number, cards, reel or graph stays linked to game state |
card_deal interpolates between the native deck and each card's own destination using your progress, with your translation/rotation offsets replacing the standard deal arc. card_flip retimes the native reveal turn and adds offsets; the face changes at progress 0.5 using the already received rank. coin_flip retimes the native flip and adds your offsets; zero endpoint offsets restore the correct final side. plinko_drop interpolates along the server's peg path using your progress; your offset replaces the standard bounce arc. It ends at the server-selected bucket and releases the ball at the native completion time.
These channels use the native result meshes. Your shell and physical controls can be custom meshes; individual card artwork, coin faces, grid symbols and text remain live platform elements. Display clips can move the full presentation for every game, including games without separate object-motion channels. The accepted target/channel vocabulary has no arbitrary node selector, property path, event handler, expression, script, shader, URL or account field.
Complete starter
Fetch the actual template and hashes before submitting; the strings below mark values to replace.
{
"version":2,
"id":"ivory-coinflip",
"game":"coinflip",
"contract":"CURRENT_16_HEX_FINGERPRINT",
"body":"BODY_HASH_FROM_ASSET_UPLOAD",
"bindings":[],
"placements":[
{"target":"display","position":[0,0,0],"rotation":[0,0,0],"scale":[1,1,1]}
],
"clips":[],
"motions":[
{"channel":"coin_flip","keyframes":[
{"time":0,"progress":0,"position":[0,0,0],"rotation":[0,0,0]},
{"time":0.5,"progress":0.4,"position":[0,0.4,0],"rotation":[0,0,1]},
{"time":1,"progress":1,"position":[0,0,0],"rotation":[0,0,0]}
]}
]
}
Modeling and preview
Import the native model as reference, design your shell around the planned interface layout, and export only the new static body/button/surface pieces. Keep dynamic readouts visible. The JSON describes placement and animation separately from GLB geometry.
Preview idle, active, disabled, occupied and settled states in the browser. Check all split hands and shared seats, maximum Mines grids, Drago difficulties, Poker holds, Dice slider extremes, Plinko drops and both coin sides. Controls carry their native disabled/hover feedback, labels and click identity when moved. The layout still needs enough room for players to see and reach them.
Exact MCP sequence
world_download({})returns the token's world, currentrevision, document, asset library and native source URLs.game_skin_contract({"game":"blackjack"})returns the current contract fingerprint, body box, ordered controls, optional parts, runtime mounts, strict schema, limits and starter manifest. Use the selected game's response, not a guessed action list.- Upload the body with
asset_upload({"name":"Ivory blackjack body","glbBase64":"..."}). The decoded input must be at most 4,000,000 bytes. Record the returnedhash; do not hash your original file and guess the remote identifier. - Upload each distinct custom button/surface the same way. All are static GLBs, processed through the same converter as props. They are library assets, not ordinary prop placements.
- Create the manifest and call
game_skin_save({"revision":CURRENT,"skin":MANIFEST,"entityIds":["blackjack:0"]}). - Store the returned
revisionandbudget. Download the draft again when another writer has changed it; never blindly increment a stale revision or overwrite the other writer's work. - Review the new appearance, then publish using the signed-in owner's normal site control. MCP credentials cannot publish or access a wallet.
entityIds is required and contains 0–64 unique existing IDs. An empty list saves the definition without assigning new stations. A nonempty list assigns the skin only to those matching game entities. Saving an existing skin ID replaces that definition; all entities already referring to that ID see the new draft definition. Existing references to a different game make the save fail rather than silently changing game type.
world_upload and authenticated draft PUT accept the same skins definitions and entity references, with identical server validation. world_edit_entities can add/remove an entity's skin field while preserving definitions already in the document. None of these operations publishes. No separate skin database or mutable asset alias is involved: the manifest is part of the revisioned world document, and the asset hashes are immutable.
Blender bridge
Install the current trusted Betworld bridge, connect your private world credential and download the world. Existing skins are included in scene metadata. The bridge previews custom bodies, socket meshes and v2 placements, including native display parts and moved button labels. Review clips, result motion, live cards and state-dependent labels in the browser.
Select only the static meshes of one skin asset and choose Upload selected skin mesh. This exports a GLB with animation, skinning, extras, cameras and lights disabled, uploads it, and stores the returned bare hash in scene["betworld_last_skin_asset"]. It does not create a separate prop entity. Repeat for distinct button/surface assets.
Put the manifest JSON into a Blender Text datablock named betworld-skin.json. Select the existing game anchor objects, then choose Save skin JSON to selected stations. The bridge calls game_skin_save with the scene's current revision, updates that revision and preserves the definition for later world uploads. Download again into a new scene to preview the applied skin. Existing scenes are preserved.
For an AI using the bridge module, the same helpers are upload_skin_mesh(bpy.context) and save_game_skin(bpy.context, skin_dict, entity_ids). Run these helpers in your local Blender scene to transfer the exported GLB and manifest.
Contract compatibility
Fetch the current contract before modeling or updating a skin. Its fingerprint identifies the game's socket geometry and runtime mounts. If a save reports an incompatible fingerprint, fetch game_skin_contract again, compare the measurements and update your manifest and artwork to match.
Ownership, storage and resource limits
An asset hash identifies bytes after canonical conversion and is scoped to the owner. The server computes SHA-256 over owner identity plus canonical bytes. The same original upload by another owner does not grant reuse rights to their library entry. Every body and binding hash, including unused skin definitions, must exist in the authenticated creator's library. Save and publish both enforce ownership and budget admission.
Every input and converted model must fit the 4,000,000-byte service limit. Textures must be embedded static PNG/JPEG/WebP, each at most 2048×2048, with an aggregate decoded budget of 4,194,304 pixels per asset. Mesh/node/accessor limits and conversion time limits apply in addition to bytes. See asset limits for details.
All converted models count against the creator's 100,000,000-byte library. Distinct used skin hashes count once toward the world's 100,000,000-byte download limit and decoded texture budget, even if the same hash is also used as a prop. Each placed body and binding adds rendered triangles and a conservative mesh-instance memory allowance. A placed v2 skin also reserves 32,768 bytes, plus 4,096 bytes per clip/motion and 2,048 bytes per placement for presentation state. Reusing the same button in ten sockets is ten geometry placements. Native game overhead remains in the estimate because the platform still loads native interfaces and collision shapes.
The existing world limits still apply: 1,000,000 entities, 500,000,000 estimated resident bytes, 20,000,000 placed triangles and 32,000,000 texture pixels. Check all returned budget values after saving. The base download allowance now reserves 25 MB for the shared native assets; custom downloads are additional. Unused skin definitions are validated for ownership but are not downloaded to a joining player's client.
Supported fields and materials
Use the manifest fields in this guide and the exact slot IDs returned by the catalog. Betworld supplies each slot's action, live state, labels and authoritative result. Your v2 JSON supplies presentation transforms and numeric animation keyframes. Model names and metadata are removed during conversion; assign controls through bindings.
Skin uploads use static GLB meshes with embedded images and supported glTF materials. Export static geometry without cameras, lights, rigs, morph targets or GLB animation tracks. Put skin animation keyframes in the JSON manifest. For troubleshooting rejected files, follow Custom assets.