PLAYER, WALLET, AND CONTROL · STEP 7

PlayerAppearance: Model Codes, Reconstruction, and Safe Fallbacks

PlayerAppearance stores public identity text, not a finished 3D character. The Player Program accepts a deliberately loose NCM envelope; the browser then chooses chain or local text, resolves the few built-in aliases, decodes NCM2 or NCM4, builds a mesh, and keeps or restores a working model when reconstruction fails. This guide follows that complete handoff and marks the current decoder, equipment, SDK, rent, and deployment boundaries honestly.

27 min read
Generated voxel riverside workshop where the canonical girl opens a small code chest while the canonical boy stands on a separate wooden model assembly platform.
The chest represents stored public text and the assembly platform represents local reconstruction: opening one does not guarantee the other succeeds. This teaching scene was generated with the canonical boy and girl and a real Chunk.js riverside material reference; its props, lighting, and visible counts are metaphors, not account bytes, chain status, or renderer output proof.
PLAYER QUESTION If my Appearance account contains an NCM code, why might I still see the previous character or the default boy—and which part of that result did the chain actually verify?
Learning path
Player, wallet, and control · Step 7 of 15
Audience
Players with no programming or blockchain background
Implemented boundary
Current dirty-worktree Player, creation, login, Play, SDK, NCM2, NCM4, and renderer behavior · separate dated Devnet account and rent observations
Reviewed
17 July 2026 · literal source excerpts, decoder-focused tests, retained raw Devnet bytes, generated visual provenance, and focused static checks
PlayerAppearance
A public 9,612-byte Player Program account containing owner links, display name, title, model kind, model text, timestamps, and twelve currently empty rich-equipment records.
PDA
A predictable address derived here from the public seed appearance-v1, the owner wallet, and the Player Program ID. A derived address is not a private vault.
Upsert
One instruction that creates a record when absent or completely rewrites its supported fields when it already exists.
modelKind
One stored byte restricted to 1 or 2. The current contract does not bind it to model geometry, and the renderer does not use it to select boy or girl bytes.
modelCode
UTF-8 text in a fixed 2,048-byte region. Passing the contract's NCM-prefix check does not prove that the text can be decoded or rendered.
Alias
A short exact label that the browser replaces with built-in model text. The current alias table contains only routes to the standard boy.
NCM2
The compact palette-and-cuboid format used by the current creation-page boy and girl. The avatar renderer adds a heuristic rig and hand anchors after decoding geometry.
NCM4
A newer self-contained character format with bounded geometry, twenty fixed bones, named actions, canonical Base64URL, and CRC32C integrity checking.
Renderer
Chunk.js code on the player's device that turns model data into vertices, indices, animation state, collision bounds, and a visible mesh.
Zero-fill
Current avatar NCM2 bit reads substitute zero when requested bits extend beyond the payload, so some truncated geometry is accepted instead of rejected.
Browser cache
Data saved on one device in localStorage. It can speed up entry or provide a fallback, but it is editable, device-local, and not chain authority.
Rich equipment
Twelve reserved 576-byte Appearance records. The decoder names their fields, but the current contract packer clears every record on each upsert.
Rent-exempt funding
Lamports held so a Solana data account can remain allocated under the observed rent schedule. The amount can change and is separate from transaction fees.
Treasury close
The current tag-9 instruction that only the configured protocol treasury may sign. It clears Appearance and sends all of its lamports to that same treasury.
Dated snapshot
RPC account bytes observed at one cluster, commitment, slot, and time. It describes that moment; it does not freeze later state or prove every dirty source file was deployed.

Key points

Stored NCM text is not a render certificate

The contract accepts valid UTF-8 beginning with uppercase NCM. It does not parse a version, Base64URL, geometry, bones, checksum, canonical form, or rendering cost.

The visible avatar has a source order and a failure order

Current Play requests chain Appearance first, then the local profile, then the built-in boy. A failed replacement keeps the last working mesh when possible before attempting the default.

NCM2 and NCM4 do not have the same safety boundary

NCM4 applies strict envelope and structural checks. The current avatar NCM2 decoder can zero-fill missing geometry bits and has no cuboid-count resource budget, even though a different source codec is stricter.

Current clients do not all build the same instruction

The website adapter supplies the six accounts required by the current processor. The older TypeScript SDK and its older test still build a five-account Appearance instruction.

PUBLIC ACCOUNT, FIXED LAYOUT

Appearance is a separate public record, not a private model file or finished mesh

For owner wallet o, the Player Program derives one Appearance PDA from the literal seed appearance-v1 and o. The account is public like other Solana program data. Its program owner controls which instructions may write it, while its stored owner field identifies the player relationship. Neither kind of ownership encrypts the display name, title, model code, timestamps, or treasury key.

The account has exactly 9,612 bytes. A 256-byte header stores magic, version, bump, initialized state, owner, linked PlayerProfile, GlobalConfig, treasury, modelKind, flags, three string lengths, the declared equipment count, and four timestamps. Fixed regions then reserve 300 bytes for display name, 96 for title, 2,048 for modelCode, and 6,912 for twelve 576-byte rich-equipment records.

That large reserved equipment region is about 71.9 percent of the account. Reserved capacity is not proof of a working feature: the current packer writes every rich-equipment state as zero and clears each payload. The fields exposed by the SDK decoder describe a format, not a current contract path that fills it.

The four user-facing fields follow independent rules. Name is at most 32 characters and 300 UTF-8 bytes using underscore, ASCII letters or digits, or U+4E00 through U+9FFF. Title may be empty and is at most 96 UTF-8 bytes. modelKind is only 1 or 2. modelCode is valid UTF-8 from 3 through 2,048 bytes beginning with the exact uppercase characters NCM. No rule connects the kind byte to the model text.

Address PDA_Player([appearance-v1, owner])

One deterministic public address per owner under the Player Program.

Total length 9,612 bytes

256 + 300 + 96 + 2,048 + 12 × 576.

Rich-equipment reserve 6,912 bytes · about 71.9%

Current packing still leaves every record empty.

Model envelope 3–2,048 UTF-8 bytes · uppercase NCM

It is not an NCM structure or rendering validation.

Generated voxel riverside archive where the canonical boy holds a small identity piece and the canonical girl opens a much larger cabinet with separate fixed storage regions.
The small identity piece and large cabinet make Profile and Appearance feel like separate public records, while the cabinet regions suggest a fixed layout. Prop sizes, shelf counts, colors, and reflections are teaching metaphors only; exact byte offsets and write authority come from the source.

ONE INSTRUCTION, THREE IDENTITY RECORDS

One upsert maintains Profile, name claim, and Appearance—then clears every reserved equipment payload

Tag 8 requires exactly six accounts: writable owner signer and payer, writable PlayerProfile, writable PlayerAppearance, GlobalConfig, System Program, and writable UsernameIndex. The current website supplies all six. The processor validates the variable instruction lengths, field rules, signer, writable flags, program identities, and the owner-derived Profile and Appearance addresses before changing state.

Within that one instruction, the program ensures or reuses PlayerProfile, writes its name, ensures or verifies the current UsernameIndex for a nonempty name, then creates or reuses Appearance. These are separate accounts changed atomically by the transaction. A later browser cache write is not part of that atomic unit.

On reuse, a valid Appearance keeps its original createdSlot and createdAt. updatedSlot and updatedAt become the current Clock values. The program then calls the complete fixed-layout packer rather than editing only changed strings. Flags, padding, reserved bytes, and all twelve rich-equipment records are rewritten.

Each 576-byte rich record is shaped for state, slot, flags, item public key, mass, grip point, grip rotation, and up to 512 model-code bytes. Current pack writes state zero, preserves only the ordinal slot byte, and zeroes the other 574 bytes. The Player Program exposes no separate Appearance instruction that fills them. Current held tools instead come through Backpack and NCF1 reconstruction, not these records.

  1. Owner approves the six-account instruction

    The wallet is signer and storage payer; Profile, Appearance, and UsernameIndex remain separate writable destinations.

  2. Profile and canonical name claim are handled first

    The current name is written to Profile and its current index is created or checked before Appearance packing.

  3. Appearance is completely packed again

    Creation metadata survives valid reuse, update metadata advances, and every rich-equipment payload returns to empty.

Generated voxel workshop where the canonical girl replaces a cyan cabinet panel, empty trays sit below, the canonical boy handles small identity pieces, and a separate treasury pedestal stands outside.
Replacing the cyan panel and leaving the lower trays empty illustrates a complete Appearance repack; the isolated pedestal introduces the separate treasury close boundary. The scene does not encode instruction order, exact slot count, signer status, or lamport transfer—the source excerpts do.

CHAIN ENVELOPE, CLIENT DECODERS

A code can fit the chain's storage rule and still be invalid, incomplete, or expensive to render

The Player contract stops after UTF-8, length, and uppercase NCM-prefix checks. It does not require a colon, recognize a version, decode Base64URL, verify a checksum, count cuboids, inspect bones, or estimate reconstruction work. Values such as NCM, NCM3:anything, or a nonexistent alias can therefore be stored even though the avatar loader cannot build them.

After exact alias resolution, the current shared avatar loader dispatches NCM4 to its strict character codec and sends other avatar text through the NCM2 decoder. NCM4 requires the exact uppercase NCM4: envelope, canonical unpadded Base64URL, valid version and flags, bounded counts and coordinates, twenty fixed bones, valid actions, no trailing ambiguity, a maximum 1,532-byte raw payload, and a matching CRC32C checksum. Those checks occur on the player's device, not inside the Player Program.

The current avatar NCM2 path is materially looser. It checks positive dimensions, scale, and palette count, but applies no cuboid-count, palette-count, dimension, canonical-encoding, trailing-byte, or reconstruction resource budget. Its bit reader zero-fills out-of-range bits. In focused read-only tests, heavily truncated standard girl text could still produce seventy boxes, and a 19-byte synthetic text declaring 10,000 cuboids produced 10,000 overlapping boxes. That is a current client memory or denial-of-service hardening gap; the 2,048-byte chain limit alone is not a render-cost guarantee.

The repository's general source NCM2 codec is stricter and throws when its bitstream ends. Passing that utility's fixture checks does not prove the actual avatar decoder rejects the same damaged text. Documentation and tests must name the consumer whose behavior they measured.

Contract parses UTF-8 + byte length + NCM prefix

No version, geometry, checksum, or cost validation.

Avatar NCM2 Missing geometry bits become 0

No current decoder resource budget or canonical check.

NCM4 Canonical envelope + CRC32C + bounded structure

Strict client validation still does not turn the code into a chain-verified mesh.

Generated voxel workshop where the canonical boy removes a flat cyan storage plate from a cabinet while the canonical girl inspects separate body parts and a frame on an assembly table.
A plate that fits the storage cabinet may still fail to become a complete body at the assembly table. The props intentionally carry no labels or status lights; only the documented validators establish which text is stored, which decoder accepts it, and which runtime work remains.

CREATION, LOGIN, AND PLAY ARE SEPARATE HANDOFFS

The creation page, browser cache, and Play refresh can briefly hold different model sources

The creation page currently offers exactly two checked-in NCM2 fixtures. It embeds the complete 353-character standard boy code and complete 451-character standard girl code, previews the selected bytes, and submits the full selected code—not a boy-or-girl alias—to Appearance. The female model therefore works without any built-in girl alias.

After the transaction helper returns, creation writes gender, full code, label, and timestamp to both legacy global localStorage keys and wallet-scoped keys. Those sequential cache writes are not part of the Solana transaction. A storage exception can occur after chain confirmation and leave only an earlier prefix of browser values; the confirmed public account cannot be rolled back by that local error.

Login first uses a wallet-scoped character cache when a username is already present. Without that shortcut, it reads Appearance and copies the chain model into global and wallet-scoped storage. Current Play can start from the local profile and then asynchronously fetch Profile and Appearance again. These latency choices allow a short interval where the creation page, login gateway, local profile, and fresh chain read do not yet agree.

For the local visible avatar, current precedence is chain Appearance.modelCode, then the local profile modelCode, then the built-in boy default. Name precedence is related but separate. A cache value is a presentation source; it is not evidence that Appearance contains the same bytes.

  1. Choose one complete standard fixture

    Creation uses the full male or female NCM2 text and sends it with name, title, and modelKind.

  2. Confirm chain work before local handoffs

    Browser caches, invite receipt, and candidate spawn are later local or separate steps; none extends the atomic scope of Appearance upsert.

  3. Let Play refresh authority asynchronously

    A cached model can appear first, then a successful chain Appearance read becomes the preferred source for the next mesh load.

Generated voxel riverside workshop with the canonical boy beside a creation counter, separate cyan packages in a browser cabinet, an empty assembly frame, and the canonical girl observing the handoff.
The separated counter, cabinet, and frame represent creation, browser storage, and Play reconstruction as different handoffs. Their spacing is the lesson; package count, cabinet order, and the unlit frame do not prove cache keys, network timing, or chain precedence.

RESOLUTION, RECONSTRUCTION, AND SURVIVAL

Aliases, NCM2, and NCM4 reach different runtime capabilities before fallback keeps play usable

The built-in alias map recognizes the empty string, NCM:peasant_guy:v1, NCM:peasant_guy_blackhair:v1, and peasant_guy. All four resolve to the same embedded standard boy NCM2. There is no current NCM:peasant_girl:v1 alias. Such text would pass the contract's NCM prefix but miss the alias table and fail the avatar decoder.

NCM2 geometry has no stored skeleton. The renderer centers its boxes above the ground, infers head, torso, arms, and legs from names and spatial position, creates pivots and hand anchors, applies procedural walking and mining motion, and can append the current basic or restored forged tool, blueprint, or held block. The character uses vertex colors and realtime light; the baked world TextureArray visible in the teaching references is not sampled as the character's own texture.

NCM4 carries bones, groups, and actions, and the shared renderer can decode a valid model, build its mesh, and sample an explicitly selected action. Current Play integration is incomplete: its normal frame state supplies moving and mining values rather than an NCM4 action or clip selection. The NCM4 mesh currently returns equipment: [] and handAnchors: {}, so the established NCM2 held-tool attachment path is bypassed. Current collision and hidden-group behavior also lack complete Play-level coverage.

When a requested local model fails, Play does not erase a mesh that already works. It tries the last active code when one exists; with no usable mesh it tries the default boy. If fallback also fails, it returns the existing mesh or null. Guardian remote avatars and Profile preview have related but separate default-boy recovery paths and caches, so one successful fallback does not repair the stored Appearance text.

Built-in aliases 4 keys → one standard boy NCM2

The empty key is included; no standard girl alias exists.

NCM2 runtime Heuristic rig + current held equipment

Procedural moving and mining are derived after geometry decode.

NCM4 in current Play Mesh and actions exist · gameplay mapping incomplete

Normal moving and mining do not select clips; equipment and handAnchors are empty.

Generated voxel reconstruction bench with the canonical boy at a model frame, the canonical girl beside separate compact model blocks, a plain fallback cube, and stored cyan packages.
The frame, compact blocks, and plain cube suggest alias resolution, two reconstruction families, and a safe fallback without pretending to show binary formats. Character identity, action support, equipment attachment, and decoder success must be read from the runtime paths rather than inferred from prop shape or color.

CURRENT CLIENT, OLDER SDK, DATED EVIDENCE

The production browser shape and older SDK differ, while Devnet proves a real modelKind mismatch

The current website chain adapter constructs the tag-8 browser instruction with six accounts, including writable UsernameIndex. The checked-in TypeScript SDK still constructs an older five-account instruction, and its older test asserts that shape. Because the current processor rejects any account count other than six, passing that test is evidence of SDK drift, not production parity. Integrators should compare any builder with the processor and deployed artifact they intend to call.

A retained confirmed Devnet snapshot at slot 476,758,441 contains ten canonical PlayerAppearance accounts. Every account is 9,612 bytes, version 1, carries the configured treasury, holds 67,790,400 lamports, and has twelve zero-state rich-equipment records. Raw model text matches the standard creation fixtures exactly: five boy codes and five girl codes, with no NCM4 in that snapshot. Eight titles are empty and two are Explorer.

The stored kind distribution is four kind 1 and six kind 2. One kind-2 account named kali contains the exact 353-character boy code. That live byte-level mismatch is direct evidence that modelKind does not select or enforce the rendered body. It is a dated observation, not a claim that every future account has this distribution.

The 67,790,400-lamport amount also matches a separate finalized 16 July 2026 minimum-balance query for 9,612 bytes. The player pays or tops up Appearance during upsert, but only the configured treasury can call tag 9. Close requires the recipient to be that same treasury, clears the data, and transfers the account's full balance. There is no current player close-and-reclaim path.

The retained deployed ProgramData allocation matched a saved ELF at the snapshot, but current Rust, Play, Chunk.js, and documentation worktrees contain later modifications. The snapshot supports its captured account and artifact claims; it does not prove every current source line is already deployed.

Instruction parity Browser 6 accounts · older SDK 5 accounts

The sixth current account is writable UsernameIndex.

Dated model sample 10 Appearance · 5 boy code · 5 girl code

Confirmed slot 476,758,441; zero NCM4 in that snapshot.

Kind mismatch 1 kind-2 record carries the boy code

Raw bytes disprove any automatic kind-to-model binding.

Observed balance 67,790,400 lamports each

Dated schedule observation, not a permanent SOL quote or player refund promise.

Generated voxel audit bench where the canonical boy inspects six cyan fixtures, the canonical girl compares a separate smaller five-part builder, and stored packages remain in a cabinet.
The unequal work fixtures suggest current browser and older SDK instruction shapes, while the cabinet represents retained account evidence. Visible fixture and package counts are narrative aids only; exact account order, sample totals, balances, and model bytes come from source and the dated raw audit.

READ THE IMPLEMENTATION

Six equations and twelve literal excerpts connect stored text to the visible villager

The formulas separate address, layout, field checks, upsert, renderability, source precedence, and close authority. Each code block is a continuous repository substring, so the plain-language explanation can be checked against the exact implementation.

One owner has a separate fixed-size Appearance address

Appearance(o)=PDA_Player([UTF8("appearance-v1"),o]); LEN=256+300+96+2048+(12×576)=9612; richShare=6912/9612≈71.9%

Derivation identifies the expected address. Program ownership, initialization, magic, version, internal relationships, and validated contents remain separate checks.

o
The player's public wallet key.
PDA_Player
Program-address derivation under the Player Program ID.
12×576
Twelve fixed rich-equipment regions, currently packed empty.

Four fields pass four independent rules

Valid=Name(chars≤32,bytes≤300,allowedRange) ∧ Title(validUTF8,bytes≤96) ∧ modelKind∈{1,2} ∧ ModelCode(validUTF8,3≤bytes≤2048,startsWith("NCM"))

The expression contains no NCM version parser, geometry decoder, checksum, reconstruction budget, or relation between modelKind and modelCode because the current contract contains none.

allowedRange
Underscore, ASCII letters or digits, or code points U+4E00 through U+9FFF.
Every listed independent condition must hold.
bytes
UTF-8 byte length, which can exceed visible character count.

Upsert preserves birth metadata but replaces the record body

Upsert_8=Ensure(Profile)+WriteName+Ensure(UsernameIndex)+CreateOrReuse(Appearance)+PackAll; created'=storedCreated∨now; updated'=now; ∀s∈[0,11],richState'_s=0

The plus signs show ordered work inside one instruction. Browser cache writes happen later and are not rolled back by this transaction.

created'
Creation slot and time after the operation.
updated'
The current Clock slot and timestamp.
s
One reserved rich-equipment slot from zero through eleven.

Contract storage and local renderability are different predicates

Stored(c)≠Renderable(c); Renderable(c)=ExactBoyAlias(c) ∨ AvatarNCM2Decode(c) succeeds ∨ StrictNCM4Decode(c) succeeds

NCM2 success here means the behavior of the actual avatar decoder, including its current zero-fill and missing resource-budget boundary. NCM4 success means its stricter canonical and structural checks passed.

Stored
Passes the Player Program's narrow modelCode envelope.
Renderable
The current shared avatar loader produces a mesh without throwing.
At least one route succeeds.

The requested source and the survival fallback have separate orders

RequestedCode=Appearance.modelCode || LocalProfile.modelCode || DefaultBoy; VisibleMesh=requestedSuccess || CurrentValidMesh || defaultBoySuccess

The double bars mean choose the first usable value. A fallback changes the local visible mesh only; it does not rewrite the public Appearance code.

LocalProfile
The browser's current local player profile object.
CurrentValidMesh
The last mesh that finished construction and was uploaded successfully.
DefaultBoy
The built-in standard boy route used when no better code is available.

Storage payer and close authority are intentionally different

UpsertPayer=owner; CloseAllowed⇔signer=recipient=configuredTreasury; close⇒data=0^9612 ∧ TreasuryLamports+=AppearanceLamports ∧ AppearanceLamports=0

The observed 67,790,400 lamports was a dated Devnet rent-exempt balance, not a source constant. No current player reclaim path exists.

0^9612
All 9,612 data bytes are filled with zero.
configuredTreasury
The treasury public key compiled into the current Player build.
The stated signer and recipient equality is required.

Fixed offsets make the 9,612-byte layout explicit

Rust programs/nicechunk_player/src/state.rs
    pub const HEADER_LEN: usize = 256;
    pub const DISPLAY_NAME_OFFSET: usize = Self::HEADER_LEN;
    pub const TITLE_OFFSET: usize = Self::DISPLAY_NAME_OFFSET + PLAYER_NAME_MAX_BYTES;
    pub const MODEL_CODE_OFFSET: usize = Self::TITLE_OFFSET + APPEARANCE_TITLE_MAX_BYTES;
    pub const EQUIPMENT_OFFSET: usize = Self::MODEL_CODE_OFFSET + APPEARANCE_MODEL_CODE_MAX_BYTES;
    pub const LEN: usize =
        Self::EQUIPMENT_OFFSET + APPEARANCE_EQUIPMENT_SLOT_COUNT * APPEARANCE_EQUIPMENT_SLOT_LEN;

Each variable string owns a fixed reserved region. The twelve equipment records begin only after the full 2,048-byte model-code region.

  1. Lines 1–5

    Build each offset from the preceding fixed-size region.

  2. Lines 6–7

    Add twelve 576-byte records to reach the total length.

Kind and model text are validated independently

Rust programs/nicechunk_player/src/state.rs
    pub fn validate_model_kind(model_kind: u8) -> Result<(), NicechunkPlayerError> {
        if model_kind != CHARACTER_MODEL_KIND_MALE && model_kind != CHARACTER_MODEL_KIND_FEMALE {
            return Err(NicechunkPlayerError::InvalidCharacterModelKind);
        }
        Ok(())
    }

    pub fn validate_title(title: &[u8]) -> Result<&str, NicechunkPlayerError> {
        if title.len() > APPEARANCE_TITLE_MAX_BYTES {
            return Err(NicechunkPlayerError::InvalidAppearanceTitle);
        }
        core::str::from_utf8(title).map_err(|_| NicechunkPlayerError::InvalidAppearanceTitle)
    }

    pub fn validate_model_code(code: &[u8]) -> Result<&str, NicechunkPlayerError> {
        if code.is_empty() || code.len() > APPEARANCE_MODEL_CODE_MAX_BYTES {
            return Err(NicechunkPlayerError::InvalidCharacterCode);
        }
        let value =
            core::str::from_utf8(code).map_err(|_| NicechunkPlayerError::InvalidCharacterCode)?;
        if !value.starts_with("NCM") {
            return Err(NicechunkPlayerError::InvalidCharacterCode);
        }
        Ok(value)
    }

The kind function receives no model text, and the code function receives no kind. The code validator stops after length, UTF-8, and uppercase prefix.

  1. Lines 1–6

    Accept only numeric kind one or two.

  2. Lines 8–24

    Validate the optional title, then accept bounded model UTF-8 beginning with uppercase NCM without decoding a format.

Tag 8 rejects any account list other than six

Rust programs/nicechunk_player/src/lib.rs
fn upsert_player_appearance(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    payload: &[u8],
) -> ProgramResult {
    if accounts.len() != 6 || payload.len() < 7 {
        return Err(NicechunkPlayerError::InvalidInstruction.into());
    }

A five-account SDK instruction fails this first shape check before the processor can perform the remaining Appearance work.

  1. Lines 1–5

    Enter the Appearance processor with supplied accounts and bytes.

  2. Lines 6–8

    Require exactly six accounts and at least the seven-byte variable-length header.

The sixth processor account is writable UsernameIndex

Rust programs/nicechunk_player/src/lib.rs
    let account_info_iter = &mut accounts.iter();
    let owner = next_account_info(account_info_iter)?;
    let player_profile = next_account_info(account_info_iter)?;
    let appearance = next_account_info(account_info_iter)?;
    let global_config = next_account_info(account_info_iter)?;
    let system_program_account = next_account_info(account_info_iter)?;
    let username_index = next_account_info(account_info_iter)?;

    if !owner.is_signer || !owner.is_writable {
        return Err(NicechunkPlayerError::InvalidPayer.into());
    }
    if !player_profile.is_writable || !appearance.is_writable || !username_index.is_writable {
        return Err(NicechunkPlayerError::InvalidWritableAccount.into());
    }

The ordered roles are owner, Profile, Appearance, GlobalConfig, System Program, and UsernameIndex. Three data accounts plus the payer must be writable as shown.

  1. Lines 1–7

    Assign each supplied AccountInfo to its fixed semantic role.

  2. Lines 9–14

    Require a writable signing payer and writable Profile, Appearance, and UsernameIndex.

Every full pack clears all twelve rich-equipment payloads

Rust programs/nicechunk_player/src/state.rs
        for slot in 0..APPEARANCE_EQUIPMENT_SLOT_COUNT {
            writer.u8(0)?;
            writer.u8(slot as u8)?;
            writer.bytes(&[0_u8; APPEARANCE_EQUIPMENT_SLOT_LEN - 2])?;
        }

For each record, state becomes zero, the slot ordinal is stored, and all other 574 bytes become zero. Earlier rich-equipment data is not preserved.

  1. Lines 1–5

    Repeat the empty-record representation for slot indexes zero through eleven.

Close clears data and transfers the entire balance

Rust programs/nicechunk_player/src/lib.rs
    {
        let mut data = appearance.try_borrow_mut_data()?;
        data.fill(0);
    }
    let reclaimed_lamports = appearance.lamports();
    **recipient.try_borrow_mut_lamports()? = recipient
        .lamports()
        .checked_add(reclaimed_lamports)
        .ok_or(NicechunkPlayerError::InvalidAppearanceData)?;
    **appearance.try_borrow_mut_lamports()? = 0;
    Ok(())

Earlier checks require the configured treasury as both signer and recipient. This block zeroes the account and transfers whatever balance is present, not only a quoted minimum.

  1. Lines 1–4

    Clear all Appearance data bytes.

  2. Lines 5–10

    Add the full balance to the validated recipient and drain Appearance.

  3. Line 11

    Return success after both changes.

The current website builder supplies UsernameIndex

JavaScript src/chain/nicechunkChain.js
    keys: [
      { pubkey: authority, isSigner: true, isWritable: true },
      { pubkey: playerProfile, isSigner: false, isWritable: true },
      { pubkey: appearance, isSigner: false, isWritable: true },
      { pubkey: deriveGlobalConfigPda(), isSigner: false, isWritable: false },
      { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
      { pubkey: usernameIndex, isSigner: false, isWritable: true },
    ],
    data: Buffer.concat([header, name, title, code]),

The browser order matches the processor and includes writable UsernameIndex as account six. Instruction data follows after those account metas.

  1. Lines 1–8

    Declare the signing owner and five non-signing accounts in the current order.

  2. Line 9

    Append tag, kind, lengths, and full string bytes.

Creation submits full model text before writing local caches

JavaScript player_creat/player_creat.js
    const result = await createPlayerAppearanceOnChain({
      playerName: name,
      gender: model.gender,
      title: "",
      ncmCode: model.code,
    });
    if (result?.submitted === false) throw new Error(result.reason || "player-appearance-not-saved");
    persistUsername(name);
    localStorage.setItem(characterStorageKeys.gender, model.gender);
    localStorage.setItem(characterStorageKeys.code, model.code);
    localStorage.setItem(characterStorageKeys.label, t(model.labelKey));
    localStorage.setItem(characterStorageKeys.createdAt, String(Date.now()));
    localStorage.setItem(walletCharacterStorageKey(characterStorageKeys.gender), model.gender);
    localStorage.setItem(walletCharacterStorageKey(characterStorageKeys.code), model.code);
    localStorage.setItem(walletCharacterStorageKey(characterStorageKeys.label), t(model.labelKey));
    localStorage.setItem(walletCharacterStorageKey(characterStorageKeys.createdAt), String(Date.now()));

The selected object contains the complete boy or girl NCM2. Chain submission is awaited first; the later global and wallet-scoped writes are independent browser operations.

  1. Lines 1–7

    Submit full selected text and reject an explicit skipped result.

  2. Lines 8–16

    Persist name and character fields locally after the helper returns.

Every built-in alias resolves to the standard boy

JavaScript chunk.js/renderer/avatar-mesh.js
export const BUILTIN_AVATAR_MODEL_CODES = new Map([
  ["", DEFAULT_PEASANT_GUY_NCM],
  ["NCM:peasant_guy:v1", DEFAULT_PEASANT_GUY_NCM],
  ["NCM:peasant_guy_blackhair:v1", DEFAULT_PEASANT_GUY_NCM],
  ["peasant_guy", DEFAULT_PEASANT_GUY_NCM],
]);

The map contains no girl alias. Creation makes the girl work by storing her full NCM2 text instead.

  1. Lines 1–6

    Map four accepted keys, including empty text, to one embedded standard boy code.

The avatar NCM2 bit reader zero-fills missing bytes

JavaScript chunk.js/renderer/avatar-mesh.js
function createBitReader(bytes, offset = 0) {
  let bitOffset = offset * 8;
  return {
    read(bits) {
      let value = 0;
      for (let i = 0; i < bits; i += 1) {
        const byte = bytes[bitOffset >> 3] ?? 0;
        const bit = (byte >> (bitOffset & 7)) & 1;
        value |= bit << i;
        bitOffset += 1;
      }
      return value >>> 0;
    },
  };
}

When geometry asks for a byte beyond the decoded payload, nullish fallback supplies zero. The read continues instead of signaling truncated data.

  1. Lines 1–4

    Create a bit-positioned reader.

  2. Lines 5–11

    Read each requested bit; missing source bytes explicitly become zero.

  3. Lines 12–15

    Return the accumulated unsigned value without an end-of-stream check.

NCM4 enforces canonical text, payload size, and CRC32C

JavaScript chunk.js/ncm/character-codec.js
function readEnvelope(code) {
  const text = String(code ?? "");
  if (!text.startsWith(NCM4_PREFIX)) throw new Error("Expected a canonical NCM4 payload.");
  const encoded = text.slice(NCM4_PREFIX.length);
  if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded) || encoded.length % 4 === 1) {
    throw new Error("Invalid canonical NCM4 Base64URL payload.");
  }
  let raw;
  try {
    raw = base64UrlDecode(encoded);
  } catch {
    throw new Error("Invalid canonical NCM4 Base64URL payload.");
  }
  if (base64UrlEncode(raw) !== encoded) throw new Error("Invalid canonical NCM4 Base64URL payload.");
  if (raw.length < 8) throw new Error("NCM4 payload is truncated.");
  if (raw.length > NCM4_MAX_PAYLOAD_BYTES) throw new Error("NCM4 payload exceeds the on-chain safety limit.");
  const body = raw.subarray(0, raw.length - 4);
  const storedChecksum = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint32(raw.length - 4, true);
  const actualChecksum = ncm4Crc32c(body);
  if (storedChecksum !== actualChecksum) throw new Error("NCM4 CRC32C checksum mismatch.");
  return { text, raw, body };
}

These are local codec checks after chain storage. Exact prefix, canonical re-encoding, minimum and maximum bytes, and checksum must all agree.

  1. Lines 1–7

    Require exact NCM4 prefix and canonical Base64URL characters.

  2. Lines 8–16

    Decode, re-encode, and enforce the bounded payload length.

  3. Lines 17–21

    Separate the checksum, recompute CRC32C, and reject a mismatch.

Play chooses chain, local, then default model text

JavaScript play/play-profile-session.js
  function currentAvatarModelCode() {
    const chainAppearance = getChainPlayer()?.snapshot?.()?.appearance || null;
    return normalizeProfileModelCode(chainAppearance?.modelCode)
      || normalizeProfileModelCode(gameState.playerProfile?.modelCode)
      || defaultModelCode;
  }

The first acceptable nonempty code comes from the asynchronously refreshed chain Appearance, followed by local profile text and the configured default route.

  1. Lines 1–2

    Read the current chain Appearance snapshot when available.

  2. Lines 3–5

    Choose chain model text, then local profile text, then default.

IMPLEMENTATION EVIDENCE

Where these claims come from

Each claim is intentionally scoped to a concrete implementation path. These references are for verification, not decoration.

programs/nicechunk_player/src/state.rs

Defines Appearance seed, magic, version, 9,612-byte offsets, field validators, complete packing, empty rich-equipment records, and creation metadata reads.

programs/nicechunk_player/src/lib.rs

Implements tag-8 six-account upsert, Profile and UsernameIndex ordering, rent funding, creation-time preservation, full repack, and treasury-only tag-9 close.

programs/nicechunk_player/src/cluster_config.rs

Pins the configured Player treasury authority used in new Appearance data and close authorization.

src/chain/nicechunkChain.js

Builds the current website's six-account instruction, applies browser field checks, fetches and decodes Appearance, and derives the name index.

sdk/nicechunk-player.ts

Retains the older five-account Appearance builder and a rich-equipment decoder, exposing both integration drift and the difference between a readable schema and an active contract writer.

tests/nicechunk_player_chunk.ts

Asserts the older SDK account shape and contains a hand-authored nonzero equipment decode fixture; it is not proof that the current program writes rich equipment.

player_creat/player_creat.js

Embeds the full standard boy and girl NCM2 codes, submits selected complete text, and writes global plus wallet-scoped browser cache values after the chain helper returns.

login/login.js

Uses a wallet-scoped character shortcut, otherwise reads Appearance and restores chain model text into global and wallet-scoped browser storage.

play/play-profile-session.js

Defines chain-Appearance, local-profile, and default model-code precedence for the local avatar.

play/play-avatar-session.js

Builds and replaces the local mesh, restores Backpack and NCF1 equipment for the established path, keeps a working mesh on failure, and attempts the default when needed.

play/play-chain-player.js

Fetches Profile and Appearance asynchronously and keeps modelKind as decoded state rather than renderer model selection.

play/play-guardian-appearance.js

Implements the separate five-minute wallet cache, bounded mesh cache, Appearance fetch, and default-boy fallback used for remote Guardian avatars.

play/player-motion-controller.js

Supplies normal moving, time, mining progress, and mining aim state without selecting an NCM4 action or clip.

chunk.js/renderer/avatar-mesh.js

Defines the boy-only alias map, NCM2 and NCM4 dispatch, loose NCM2 zero-filled bit reader, heuristic NCM2 rig and tools, and current NCM4 empty equipment and hand-anchor fields.

chunk.js/renderer/avatar-action.js

Resolves and samples explicitly selected NCM4 actions, bone rotations, and visible groups.

chunk.js/ncm/character-codec.js

Defines NCM4's 1,532-byte payload ceiling, twenty bones, bounded geometry and actions, canonical Base64URL envelope, and CRC32C validation.

src/vox/ncm.js

Provides the stricter general source NCM2 decoder whose truncated-bit rejection must not be confused with the avatar renderer's zero-fill behavior.

chunk.js/renderer/webgl2-renderer.js

Shows avatar vertex color and realtime lighting behavior separately from the world's baked terrain texture array.

public/media/vox/chr_peasant_guy_blackhair.ncm

The checked-in 353-character standard boy NCM2 fixture used by current creation and built-in fallback.

public/media/vox/chr_peasant_girl_orangehair.ncm

The checked-in 451-character standard girl NCM2 fixture submitted as full text because no girl alias exists.

chunk.js/tests/ncm-character.test.mjs

Exercises shared character format behavior; its passing scope does not add Player Program validation.

chunk.js/tests/ncm4-avatar-renderer.test.mjs

Exercises NCM4 decode, mesh construction, groups, and explicit action sampling outside the incomplete normal Play mapping.

docs/audits/character-creation-devnet-2026-07-16.json

Retains finalized Devnet rent queries, standard model hashes and dimensions, deployed ProgramData evidence, and source-versus-deployment limitations.

docs/audits/player-profile-devnet-2026-07-16.json

Retains confirmed slot-476,758,441 raw accounts used to re-derive ten Appearance addresses, balances, field distributions, model bytes, titles, and 120 empty rich states.

docs/visual-provenance/player-appearance-codes.json

Records exact imagegen prompts and edit lineage, source and output hashes, processing decisions, and the one-to-one teaching role of all seven visuals.