# Game skin contract v2 [Read as Markdown](/raw/game-skin-contract.md) · [JSON schema](/game-skin.schema.json) · [Per-game catalog](/game-skin-catalog.json) 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: ```json {"slot":"control.hit","asset":"HASH_RETURNED_BY_ASSET_UPLOAD"} ``` Use a `control.` from the selected game's `controls` catalog or a `part.` 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`: ```json { "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.` | 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`: ```json { "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. ```json { "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. ```json { "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 1. `world_download({})` returns the token's world, current `revision`, document, asset library and native source URLs. 2. `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. 3. Upload the body with `asset_upload({"name":"Ivory blackjack body","glbBase64":"..."})`. The decoded input must be at most 4,000,000 bytes. Record the returned `hash`; do not hash your original file and guess the remote identifier. 4. 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. 5. Create the manifest and call `game_skin_save({"revision":CURRENT,"skin":MANIFEST,"entityIds":["blackjack:0"]})`. 6. Store the returned `revision` and `budget`. Download the draft again when another writer has changed it; never blindly increment a stale revision or overwrite the other writer's work. 7. 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](/downloads/betworld_blender.py), 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](/assets/) 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](/assets/).