# Woodland traveler character creator Use `/character` at any time while connected, or open **Character appearance** from the inventory. Creation is opt-in: joining restores the saved appearance without opening the creator. Existing location selection and field-note join forms retain priority. ## A quiet traveler, not a costume catalogue All choices are inclusive and independent, with no gender, identity or stat fields. Compact, lean, balanced, broad and sturdy silhouettes have identical mechanics, height, camera, collision, reach, movement and held-tool anchors. Hair, layered field clothes, cloaks, belt gear and packs use original low-poly geometry with hand-drawn, muted woodland pixel art. Four tabs keep the 14 × 11 form manageable: | Tab | Choices | | --- | --- | | Face | 5 body silhouettes, 6 skin tones, 5 face-line patterns, 5 eye colors | | Hair | 10 styles, 6 shared hair/facial-hair colors, 7 facial-hair styles | | Clothing | 7 garments, 8 cloth colors, 4 trousers, 3 boot leathers | | Finishing | 9 accents, 8 accent colors, 5 face-detail patterns | Dropdowns use readable names, not gender categories or free-form colors. Cloth and accent palettes are sage, teal, slate, mauve, ochre, rust, linen and charcoal. Transparent `none` layers and colors under absent hair/accents mean the raw selection combinations are not all visually distinct; no uniqueness count is claimed. ## Preview and explicit consent The form contains a real Luanti `model[]` preview, not a screenshot, external browser or URL. It uses one composited 64 × 64 texture and the selected B3D. The preview turns continuously and supports mouse dragging; Front, Profile and Back reset its camera angle. These controls never rotate the actual player. Changing a dropdown, switching tabs, **Randomize** and **Reset draft** affect only a private session draft. Randomize chooses from every fixed allowlist using bounded `math.random`; it is not security randomness and introduces no secrets. Reset copies the defaults rather than modifying shared state. **Cancel** or Escape discards the draft, preserving both the real appearance and saved bytes. Only **Save / Apply** commits. No need, inventory, identity or physics values are changed. Drafts disappear when the session ends. ## Version-1 storage and recovery The only new persistent value is the engine-owned player metadata string `hearth:appearance`. It is independent of the existing global save schema and world/mod storage. A complete flat JSON object contains exactly `version:1` and these string selections: `body`, `skin`, `face`, `eyes`, `hair`, `hairColor`, `beard`, `garment`, `cloth`, `trousers`, `boots`, `accent`, `accentColor`, `detail`. Defaults match the [authoritative art contract](art-direction.md): balanced, sand, calm, hazel, sidepart, chestnut, none, tunic, teal, earth, walnut, scarf, sage, none. An absent value uses defaults without writing metadata. Invalid, unknown-version, missing/extra-field, malformed, nested or over-2048-byte values show safe defaults and a fixed warning. Original corrupt bytes are **not overwritten**, and appearance errors never kick a player. Only an explicit successful Apply replaces that value. Back up the world before manual recovery. Encoding and decoding use `core.write_json` / `core.parse_json(raw,nil,true)`; parser errors do not log player payloads. There is no executable deserialization, custom path, uploaded texture or free-form hexadecimal color. Every mesh and layer filename is built solely from fixed IDs. Compositing order is skin, face, eyes, garment, trousers, boots, hair, beard, accent, detail; accents layer above hair so a selected hood frames and covers it. The model includes an original draped mantle, belt, pack, projecting nose and ears. Face overlays are skin-neutral; there is no global color tint. The held-tool material remains the existing compatibility texture. Apply serializes and checks the roundtrip, then writes metadata **before** publishing `mesh` and `textures`. A thrown/false metadata write or serialization failure keeps the previous published appearance and saved session selection, leaving the draft available to retry. Engine metadata durability still follows normal engine player saving; this is not a new disk transaction service. ## Integration and validation boundary [The factory](../mods/hearth/character.lua) receives `(hearth, api)`, registers its own command/receive-fields callback, and exports `apply`, `on_join`, `show`, `handle`, `defaults`, `options`, `validate`, `decode`, `encode`, `preview` and snapshot-only `state`. Bootstrap instantiates it before the interface and players; the player join handler calls it after base property initialization. Only the current engine-supplied connected player object and its own session can edit an open draft. Target names, arbitrary fields, hidden-tab selections and invalid dropdown indices are rejected as a whole event. All displayed data is formspec-escaped; text and input sizes are bounded. CodeGuard input/storage guidance was applied: allowlisted keys and selections, bounded parsing, session ownership, preserved corrupt data and sanitized errors. No credentials, certificate handling, cryptography, network calls or dependencies are introduced. No authentication or database infrastructure is changed. Focused offline Lua harnesses cover character behavior, player join integration, inventory navigation and bootstrap order. Mocks model native JSON through registered exact fixture strings, not executable Lua. Native engine rendering, mouse behavior and visual fit still require client review; no full-engine or live-world test is claimed by this change.