PLAYER, WALLET, AND CONTROL · STEP 6

PlayerProfile PDAs: Identity, Equipment Identities, and Chain Skills

A NiceChunk player is not stored in one all-purpose account. Current source keeps a compact PlayerProfile, separate PlayerEquipment custody, immutable ForgedItem models, a much larger Appearance record, a unique-name claim, Backpack inventory, program-scoped progress, and browser presentation state. This guide explains what each account proves and labels its older Player-only Devnet snapshot as history rather than current equipment evidence.

24 min read
Generated voxel workshop: the canonical boy holds a cyan marker while the canonical girl compares a drawer, cabinet, Backpack, and two progress instruments.
The separate drawer, cabinet, Backpack, and instruments introduce one central idea: the player experience is assembled from several records and local sources. These physical props are teaching metaphors made with the canonical villagers and real Chunk.js material language; they are not account decoders, exact field counts, ownership proofs, or evidence that every source agrees.
PLAYER QUESTION When the Profile screen shows my name, avatar, Backpack, equipment, position, and skills, which parts really came from PlayerProfile—and which came from somewhere else?
Learning path
Player, wallet, and control · Step 6 of 15
Audience
Players with no programming or blockchain background
Implemented boundary
Current-worktree Player, Backpack, Market, Chunk, Smelting, Game, Play, SDK, and renderer source behavior · separate confirmed Devnet evidence for Player Program accounts
Reviewed
30 July 2026 · current source, literal excerpts, focused static checks, generated visual provenance, and retained 16 July Devnet account evidence
Owner
The player's wallet public key. It is public identity data, not a secret key.
Program
A Solana smart contract. Each program validates only the accounts and instructions defined by its own code.
PDA
A predictable address derived from public seeds and a program ID. Derivation alone does not create an account or prove its account owner; initialization and ownership checks are separate.
Program scope
The program ID included in PDA derivation and ownership checks. It is why Chunk progress and Game-scoped Smelting progress remain separate.
PlayerProfile
The current 773-byte Player Program account for owner and world binding, position, fixed attributes, nine equipment identity fields, one equipped-Backpack address, forging history, and a bounded name.
PlayerAppearance
A separate 9,612-byte Player Program account for display name, title, model kind, model code, and twelve reserved rich-equipment records.
UsernameIndex
A 256-byte account addressed by a canonical name hash. It links one current name claim to an owner and PlayerProfile.
Backpack
A separate inventory account whose dense records hold resources and forged items. Its contents are not copied into PlayerProfile.
Account owner
The Solana program allowed to write an account. This is different from the player wallet stored inside Backpack data as its owner field.
Record index
A zero-based position inside the Backpack's dense record array. It is not the same as an equipment slot or an item identifier.
item_pda field
A 32-byte field inside an item record. Current verified forging stores the unique immutable ForgedItem PDA derived from creator and item ID; Profile equipment does not copy this value.
PlayerEquipment
A 7,040-byte Player Program PDA with nine custody slots. Each active slot stores source Backpack data, a complete 80-byte record, and up to 640 verified model bytes.
Equipment identity
The 32-byte equipment-v2 hash of source Backpack address and complete custodied record. PlayerProfile publishes one identity per slot while PlayerEquipment holds the actual record.
PlayerProgress
A 128-byte progress layout derived under a specific program. Chunk and Game can hold separate accounts with the same seed words.
Forging XP
XP stored directly in PlayerProfile after Backpack calculates a forge outcome and sends the gained amount to Player.
Derived XP
A historical browser calculation from local activity counters. Current Profile skill state no longer accepts it as displayed XP or an effect level.
localStorage
Browser storage on the current device. It can cache presentation and Backpack selection, but current Profile skills do not read it as XP authority.
Confirmed read
The RPC commitment used by current browser helpers and this dated snapshot. It is a useful observation level, not a permanent guarantee.
Dated snapshot
An account inventory captured at one slot and time. Later transactions can change counts and field values.
Provenance
A statement of where a displayed value came from: a named account, a runtime value, browser storage, or a derived calculation.

Key points

PlayerProfile is an anchor, not the whole character

Its 773 bytes do not contain Backpack inventory, full avatar geometry, a unique-name account, both progress accounts, wallet balance, hotbar selection, or most Profile-screen skills.

Profile equipment publishes identity; PlayerEquipment holds custody

Equip atomically moves or exchanges the complete Backpack record into PlayerEquipment, then stores only its equipment-v2 identity in Profile. Current forged item_pda instead names one unique immutable ForgedItem account containing canonical NCF1 bytes.

The Profile screen is a composite view

Current refresh batches Chunk Progress, Smelting Progress, PlayerSkills, and SkillRuleTable data, but the Profile skill panel takes all ten displayed XP and levels from PlayerSkills. Profile forging history remains a separate account fact.

Program scope decides which progress was actually read

Equal seed words under Chunk, Smelting, and Skills still produce different PDAs. The current reader validates each domain before source synchronization and never substitutes a browser value for PlayerSkills.

PROGRAM-SCOPED IDENTITY

One wallet resolves to several records with different jobs

The active website configuration points at Devnet. Profile, Appearance, UsernameIndex, and Session belong to the Player Program. Precision Gathering and Exploration progress belong to the standalone Chunk Program. Backpack, Smelting, and Market are currently routed through the unified Game Program. A PDA includes its program domain, so identical seed words under Chunk and Game still produce unrelated addresses.

PlayerProfile is the compact owner-and-world anchor. PlayerAppearance carries visible identity and model data. UsernameIndex carries the canonical name claim. Backpack carries dense inventory records. PlayerProgress carries skill history under the program that updated it. These records can repeat the same owner or Profile key, but no program automatically joins them into one database row.

For an ordinary player, the useful rule is simple: one wallet can point to several public boxes, and each box answers a different question. Reading one box does not prove the other boxes exist, are current, or agree.

Compact anchor PlayerProfile · 773 bytes

One current player-v7 PDA under the Player Program.

Visible appearance PlayerAppearance · 9,612 bytes

A separate appearance-v1 PDA under the Player Program.

Split progress Chunk PDA ≠ Game PDA

The same progress seed tuple remains program-scoped.

Generated voxel archive: the canonical villagers stand among a desk, mirror cabinet, separate shelves, and two cyan-topped pedestals.
The desk, mirror cabinet, shelves, and two isolated pedestals make the records feel separate even though the same villagers relate to all of them. The natural mirror reflection and visible prop counts are illustrative only; the formula and source determine real PDA addresses, program scope, and byte layouts.

COMPACT ACCOUNT, COMPOSITE SCREEN

PlayerProfile stores public fields and checkpoints, not the whole Profile screen

Current initialization writes the NCKPLY01 magic, version 7, owner, GlobalConfig, world ID, position zero, and six defaults: health 100, energy 100, stamina 100, mining power 1, build power 1, and defense 0. It also creates nine empty equipment identity fields, an empty equipped-Backpack address, timestamps, zero forging history, and a bounded name region. No current Player instruction changes those six default attributes.

Position is three signed 32-bit integers. The Player Program checks only Y against Core's minimum and maximum build bounds. Current Play does not read Profile position when choosing a spawn and does not save ordinary walking to chain. Query pose, local position storage, or the default Guardian start chooses Play startup instead.

The confirmed resource-mining path is the current place that may save a Profile checkpoint. If the verified mine transaction did not already carry that save, Play can request a forced supplemental position update. This makes Profile position a sparse chain checkpoint, not a birth coordinate, live tracker, or proof of where the avatar is standing now.

The visible Profile panel then adds information from elsewhere: its level averages effective client skill levels, reputation uses local confirmed-action counters, region comes from runtime position, balance comes from the wallet, and stat cards come from client skill effects. These cards are useful, but they are not direct decodes of the six fixed Profile attributes.

Initialized attributes 100 · 100 · 100 · 1 · 1 · 0

The current Player instruction set has no write path for these six u16 values.

Position i32 X · i32 Y · i32 Z

Only Y is contract-bounded; current chain saves are tied to the verified resource-mine path.

Startup Not read from Profile

Current Play chooses a query pose, local saved position, or default start.

Generated hillside workshop: the canonical boy holds a small ledger while the canonical girl arranges a mirror, clothes stand, Backpack, tool, and map chest.
The small ledger represents the compact Profile while the larger preparation area represents information the screen obtains elsewhere. The mirror reflection, map texture, and prop arrangement are narrative metaphors; they do not encode exact offsets, prove a chain checkpoint, or show a spawn rule.

NAME CLAIM, VISIBLE IDENTITY, AND MODEL CODE

A unique name claim and a visible appearance follow separate rules

PlayerProfile reserves 300 bytes for a name, but the contract accepts at most 32 characters drawn from underscore, ASCII letters or digits, and U+4E00 through U+9FFF. The browser precheck uses the broader Unicode Han script class, so some extension characters can pass the browser and still fail the program. For a nonempty name, the program hashes the UTF-8 bytes after lowering ASCII A through Z only and derives UsernameIndex from that hash.

Changing a Profile name does not close the old UsernameIndex, and clearing the name does not release earlier claims. A case-only rename reaches the same index and validates ownership, but an existing index is not rewritten with new display capitalization. A string visible in Profile or Appearance is therefore not, by itself, proof that the matching canonical index exists.

Appearance upsert is the complete visible-identity path: it writes the validated name into Profile, claims or reuses the current UsernameIndex, and packs display name, title, model kind, and model code into Appearance in one instruction. A name-only update changes Profile but not Appearance. Current Play prefers Appearance.displayName, so an older Appearance value can continue masking a newer Profile-only name.

The modelCode contract is deliberately weak: it accepts 3 through 2,048 valid UTF-8 bytes beginning with the case-sensitive prefix NCM. It does not validate an NCM version, decode geometry, bind modelKind to a body, or require a canonical villager. The current Chunk.js loader accepts built-in aliases and renders both NCM2 and NCM4. A prefix-only value such as NCM can pass the program and still fail rendering.

The player funds Appearance storage, but only the configured treasury can close that account and receive its lamports. PlayerProfile and UsernameIndex have no close instruction. Storage funding should not be presented as an owner-controlled refundable deposit.

  1. Validate the exact contract character set

    Apply 32-character, 300-byte, ASCII, underscore, and U+4E00–U+9FFF limits before submission; the browser's broader Han test is not final authority.

  2. Claim or reuse the canonical name address

    A nonempty name needs the SHA-256-derived UsernameIndex to be absent or already bound to the same owner and current Profile.

  3. Update Profile and Appearance together when visible identity changes

    The full Appearance upsert keeps the two records aligned; a name-only write intentionally leaves Appearance untouched.

Generated identity stall: the canonical girl faces a lit mirror and wardrobe while the canonical boy holds a blank token at a separate counter.
The bright mirror, small blank token, and separate notebook make Appearance, Profile name, and local fallback feel like different sources with an order of precedence. The mirror is not a portal, the token is not a cryptographic proof, and clothing counts do not represent account fields.

IDENTITY, RECORD, MODEL, AND CUSTODY

Profile publishes equipment identity while PlayerEquipment holds the record

Player tag 13 is a full custody instruction, not a two-field Profile write. The wallet signs; current owner-derived Profile and PlayerEquipment are writable and bound to the same Core-owned GlobalConfig; the Backpack is a valid owner-derived account under the configured unified Game Program; MaterialPhysics, System Program, executable Game Program, and the equipment-transfer authority PDA complete the nine-account family. A missing PlayerEquipment account is created directly at its final 7,040-byte layout.

For an empty target slot, Game densely removes the selected complete 80-byte record from Backpack. For an occupied target, Game replaces the selected Backpack position with the old custodied record, exchanging the two without changing item count. Player stores the incoming record, source Backpack address, former index, custody flags, and any verified forged model bytes in one 768-byte PlayerEquipment slot. Backpack and Player writes share one transaction, so a half-move cannot commit.

PlayerProfile receives neither the complete record nor item_pda. It receives hashv(equipment-v2, source Backpack address, complete record). Empty slots publish zero. Player tag 14 swaps two complete PlayerEquipment slots and rewrites both Profile identities. Backpack index 255 reverses custody by appending the selected record to a valid owner Backpack before clearing the equipment slot and Profile identity.

Current verified forging separately derives ForgedItem from forged-item-v1, creator, and item ID. That 752-byte immutable account stores canonical version-15 NCF1 bytes; the forged Backpack record's item_pda points to it. Production validates the model account and passes its NCF1 bytes into tag 13, where PlayerEquipment verifies and stores them again for fresh-client rendering while equipped. Item identity, equipment identity, and Profile address are therefore three different keys.

Market equipment source reads the chosen custodied PlayerEquipment slot directly, reconstructs equipment-v2 from the embedded record, and requires the matching Profile slot to publish it. It does not scan Backpack for an equal item_pda. Backpack source instead copies and densely removes the exact selected Backpack index. Either source stores the complete 80-byte record in Listing and performs source removal atomically.

Current durability settlement exposes one precise edge. Player tag 15 subtracts forged durability from the record inside PlayerEquipment, but it receives Profile read-only and does not republish the identity even though durability is part of the hashed record. The worn equipment remains usable for mining and can be unequipped, but direct equipment-source Market release rejects the stale Profile identity until a swap or unequip-and-re-equip writes a current one.

PlayerAppearance still reserves twelve 576-byte rich-equipment records, and current Appearance packing writes them empty. Production Profile UI combines compact Profile fields, chain PlayerEquipment, synchronized Backpack data, Appearance, program-scoped progress, and browser presentation. The generated workshop image is a teaching metaphor; exact custody and identity come from the account processors.

Forge writes record.item_pda = unique ForgedItem PDA

forged-item-v1, creator, and item ID derive the immutable model account.

Equipment custody PlayerEquipment = complete record + model

The selected record leaves or exchanges with Backpack atomically.

Profile publishes hashv(equipment-v2, Backpack, record)

Market checks this identity against the selected custodied slot.

Durability edge tag 15 changes record, not Profile

The equipment identity becomes stale until another identity-writing operation.

Generated equipment workshop showing the source moment before a forged pickaxe moves from the girl's open Backpack into a custody rack while the boy prepares a cyan Profile identity marker.
Read the open Backpack as the source before transfer, the wall rack as PlayerEquipment custody, and the cyan marker as the derived Profile identity. The still image cannot show the atomic removal or exchange, so exact slot counts, model validation, Market checks, and item uniqueness come from source rather than prop placement.

TEN CHAIN SKILLS, THREE SOURCE DOMAINS

The Profile panel now reads authoritative PlayerSkills instead of browser XP

The Profile panel defines ten skills, and current displayed XP and levels come from the separate PlayerSkills PDA. buildProfileSkillState accepts only chain XP, chain levels, and chain thresholds, normalizes them, labels the result source chain, and performs no localStorage, derived-counter, or maximum-across-sources merge.

Current refresh still fetches three progress domains together: legacy Chunk PlayerProgress, legacy Smelting PlayerProgress under the configured Smelting program, and PlayerSkills under the Skills Program. It also fetches SkillRuleTable. Every present account is validated against its expected program owner, owner-derived PDA, GlobalConfig, final layout, version, initialized state, and domain-specific fields.

fetchPlayerProgress returns legacy precisionGatheringXp, smeltingXp, and explorationXp for diagnostics and source synchronization, but production Play assigns Profile skillXp and skillLevels only from the decoded PlayerSkills maps. If PlayerSkills is absent, all ten values are zero. Thresholds come from SkillRuleTable; when the PlayerSkills rule revision differs, the helper force-refreshes that table before returning revision status.

Mining, Smelting, forging, and other verified flows append Skills synchronization instructions with their authoritative source accounts. Backpack records one XP for every completed forge and updates Profile forging history, while Skills synchronization translates reviewed source state into the ten-skill account. The Profile's historical forging fields and legacy Progress accounts are therefore evidence sources, not alternate UI XP winners.

Client effects and Profile cards consume the same chain levels returned by PlayerSkills. A displayed skill can still have an effect that another program does not enforce; that boundary belongs to the displayed-stats guide. What no longer happens here is quietly promoting a larger browser number over chain state.

Displayed skill source PlayerSkills PDA

All ten Profile skill XP and level values come from the validated chain account.

Threshold source SkillRuleTable PDA

A revision mismatch triggers one forced table refresh before return.

Reviewed source domains Chunk · Smelting · Skills

Legacy Progress remains available for validation and synchronization, not UI max-merging.

Generated grassy clearing: the canonical villagers tend separate cyan progress vessels in quarry and smelting workshops, with notebooks between them.
The separated workshops represent Chunk and Smelting source domains, while the center ledgers represent the Skills account and rule table that consolidate current Profile values. The vessel and notebook count is illustrative; exact PDA domains, revisions, byte layouts, and UI source selection come from code.

SOURCE-BY-SOURCE PROVENANCE

Treat every Profile card as a claim with its own source

The current Profile decoder reads position, fixed attributes, nine equipment identities, equipped Backpack, forging history, and name. Production separately fetches PlayerEquipment and reconstructs the nine chain-backed hotbar positions from its custodied records. The Profile equipment panel shows the currently selected hotbar record as main hand and synchronized Backpack as back slot; its anatomical head, armor, ring, and similar rows remain conceptual empty or locked UI rows rather than direct mappings of the nine numeric chain slots.

Backpack synchronization is cache-first. If a wallet-scoped cached Backpack address still loads as a valid owner Backpack, the helper returns it before reading Profile.equippedBackpack. Only a missing or invalid cache falls back to the current Profile pointer. A screen can therefore show a valid Backpack without proving that it was freshly reconciled with the current Profile selection.

Identity is also layered. Appearance display name wins over Profile name; local profile values and built-in villager aliases can supply fallbacks. Runtime position, wallet balance, and local activity counters supply other cards, while skill XP, levels, thresholds, and active effects now come from the validated PlayerSkills and SkillRuleTable chain snapshot. The current Profile panel has no visible control wired to the available savePlayerName or savePlayerAppearance helpers, so helper existence is not proof that players can edit identity there.

The browser chain module and exported TypeScript SDK do not currently agree on every name instruction. Static source shows the SDK building three accounts for a nonempty name update where the processor requires five, and five accounts for Appearance upsert where the processor requires six. The browser module constructs the five- and six-account forms. Existing tests assert older SDK lists, so passing those tests does not establish deployed-contract parity. No unsupported Devnet simulation result is claimed here.

A trustworthy Profile screen does not need one giant account. It needs labels and reconciliation: name the account or runtime source, fetch progress under the correct program, distinguish PlayerEquipment custody from Profile identity, and keep local presentation values visibly separate from chain state.

  • Identity: state whether Appearance, Profile, local profile, or built-in fallback won.
  • Backpack: state whether the value came from wallet cache or a fresh Profile pointer fallback.
  • Equipment: separate Backpack, PlayerEquipment record and model, Profile identity, empty Appearance reserve, and selected hotbar presentation.
  • Skills: show PlayerSkills XP and levels plus SkillRuleTable thresholds; do not merge browser XP into chain state.
  • Position: distinguish runtime location, local startup save, and sparse chain checkpoint.
  • SDK: compare constructed account lists with the processor contract before treating a test as integration proof.
Generated records workshop: the canonical girl lifts a ledger beside separate trays of Profile-screen props while the canonical boy watches by a mirror.
Each object remains in its own tray so the viewer can ask where each visible claim came from. The pouch, compass, abacus, cube, Backpack, ledger, mirror, and rail are source metaphors only; they are not UI controls, verified account contents, or proof that the current browser reconciled them.

RETAINED PRE-EQUIPMENT CONFIRMED SNAPSHOT

The 16 July snapshot preserves Profile lineage, not current equipment use

At confirmed context slot 476,758,441 on 16 July 2026, the retained audit captured all 47 Player-Program-owned accounts with raw Base64 data and hashes. Nineteen carried Profile magic: eleven matched the current owner-derived player-v7 PDA, while eight were earlier records. The remaining recognized accounts were ten Appearances, seven UsernameIndexes, ten Sessions, and one InviteIndex page.

The eight earlier Profile records consisted of four 473-byte version-2 records, three 773-byte version-3 records, and one 773-byte version-4 record. Length alone is therefore insufficient: four earlier records use the current 773-byte length. Current writes derive player-v7, so the canonical owner-derived address, version, layout, owner, and GlobalConfig all matter.

Among the eleven current Profiles, ten had moved away from origin, five stored an equipped-Backpack pointer, none stored any nonzero value in the nine equipment fields, and two had forging history. All 120 reserved rich equipment states across ten Appearances were zero. All ten retained Sessions were expired at the snapshot and had action count zero. No 7,040-byte PlayerEquipment account existed in the captured Player account set, so this observation predates the current custody architecture and cannot measure current equipment use.

The full upgradeable-loader ProgramData account was 172,925 bytes: 45 bytes of loader metadata followed by a 172,880-byte ELF allocation. That ELF allocation hashed to 9158af26bb95eea3848b10ab06dbee822f8c0f30552e7538b494ac6a2ef9f32a and matched the retained local 172,880-byte artifact. The byte match does not prove that the dirty source worktree reproducibly built it.

The archive artwork deliberately does not reproduce these counts. Boxes can make old and current records intuitive, but only the retained audit establishes the dated numbers, raw account bytes, hashes, context slot, and interpretation boundaries.

Recognized accounts 47 at slot 476,758,441

Raw account bytes and hashes are retained in the dedicated confirmed snapshot.

Profile magic 11 current · 8 earlier

Four earlier records are also 773 bytes, so address derivation matters.

ProgramData 172,925 total · 172,880 ELF

The smaller number excludes 45 bytes of loader metadata.

Generated archive courtyard based on the Chunk.js hillside: the canonical villagers inspect clean and weathered chests beside a survey tripod.
The clean and weathered chests make current and earlier records easy to imagine, and the tripod suggests a point-in-time observation. Chest counts, shapes, and the tripod are narrative devices only. They are not the Player Program, an RPC client, account bytes, or evidence for the audited totals.

READ THE IMPLEMENTATION

Six equations and eight literal excerpts connect the player-facing story to code

The equations identify addresses, byte layouts, browser precedence, unique ForgedItem identity, PlayerEquipment custody, Profile publication, and chain-only skill presentation. Each source block is copied as one continuous substring from the current repository so readers can compare the explanation with the implementation.

One owner resolves to several program-scoped addresses

Profile(o)=PDA_P([UTF8("player-v7"),o]); Equipment(o)=PDA_P([UTF8("player-equipment-v1"),o]); Appearance(o)=PDA_P([UTF8("appearance-v1"),o]); Username(n)=PDA_P([UTF8("player-name-v1"),SHA256(asciiLower(n))]); ChunkProgress(o)=PDA_C([UTF8("player-progress"),GlobalConfig,o]); SmeltingProgress(o)=PDA_G([UTF8("player-progress"),GlobalConfig,o])

P is the Player Program, C is the standalone Chunk Program, and G is the unified Game Program used by current Smelting. Changing the program domain changes the PDA even when owner and seed bytes stay the same.

o, n
The owner public key and validated player-name string.
asciiLower
Lowercase ASCII A through Z only; preserve every other accepted UTF-8 byte.
P, C, G
Player, Chunk, and unified Game program domains.

Profile publishes compact identities while Equipment stores custody

Profile=773 bytes; Equipment=128+(9×768)=7040 bytes; Appearance=256+300+96+2048+(12×576)=9612 bytes

Profile spends 288 bytes on nine 32-byte equipment identities. PlayerEquipment stores nine complete custody slots. Appearance reserves 6,912 bytes for twelve rich equipment records, although current packing resets all twelve to zero.

9×768
Nine authoritative PlayerEquipment custody slots.
12×576
Twelve reserved Appearance rich-equipment records.
9×32
Nine compact identities published inside PlayerProfile.

Visible identity follows a source precedence

chainName=Appearance.displayName || Profile.playerName; HUDName=chainName || LocalProfile.name || "Local Miner"; AvatarCode=Appearance.modelCode || LocalProfile.modelCode || builtInVillager

The double bar means the first nonempty value. This selection explains why a newer Profile-only name can remain hidden by an older Appearance and why a rendered avatar can exist even when no accepted Appearance model is available.

||
Choose the first nonempty value.
chainName
Appearance display name first, then compact Profile name.
LocalProfile
Browser-stored profile data used as a fallback or preview source.

Forged item identity and equipment identity are separate

F_j=PDA_G([UTF8("forged-item-v1"),o,LE64(itemId_j)]); ForgedRecord_j.item_pda=F_j; Profile.equipment[s]=hashv(UTF8("equipment-v2"),Equipment[s].sourceBackpack,Equipment[s].record80)

F_j identifies immutable canonical NCF1 under the Backpack or unified Game program. The equipment-v2 value binds current custody bytes and source Backpack for Market validation. Neither value is the PlayerProfile address.

j
One current verified forged output.
F_j
The unique immutable ForgedItem account for output j.
record80
The complete record currently held in PlayerEquipment custody.

Equip moves or exchanges complete custody atomically

E_before[s]=empty ⇒ E_after[s]=B_before[i] ∧ B_after=denseRemove(B_before,i); E_before[s]=old ⇒ E_after[s]=B_before[i] ∧ B_after[i]=old; Profile_after[s]=identity(E_after[s])

The signed Player instruction validates current Profile, owner-derived PlayerEquipment, unified-Game Backpack, GlobalConfig, MaterialPhysics, System Program, and transfer authority. Forged model bytes also require final NCF1 size, version, and design-hash checks.

E[s]
The complete record and model state in PlayerEquipment slot s.
denseRemove
Remove the selected record, shift later Backpack records left, clear the old tail, and reduce item count.
identity
The equipment-v2 hash published through PlayerProfile after custody settles.

The UI normalizes one chain skill state

XP_UI(s)=PlayerSkills.xp[s]; Level_UI(s)=PlayerSkills.level[s]; Threshold_UI(s)=SkillRuleTable.thresholds[s]; ForgeOutcomeXP=1 per completed forge

No browser or derived XP participates in buildProfileSkillState. The one-XP forge outcome belongs to Backpack state and can enter the ten-skill system only through the reviewed chain synchronization path.

s
One visible Profile skill.
PlayerSkills
The owner-derived Skills Program account carrying all ten current XP and level values.
grade
The final forge grade from one through ten calculated by Backpack.

Forge passes the immutable ForgedItem address as item_pda

Rust programs/nicechunk_backpack/src/lib.rs
    let outcome = {
        let mut backpack_data = backpack.try_borrow_mut_data()?;
        BackpackAccount::forge_equipment_from_verified_materials(
            &mut backpack_data,
            owner.key,
            indexes,
            item_id,
            design_hash,
            forged_item.key,
            forging_level,
            clock.slot,
            requirements,
        )?
    };

forged_item.key becomes the Backpack record's item_pda. It is an item-specific immutable PDA, not a shared PlayerProfile key.

  1. Lines 1–2

    Open a scoped mutable borrow of the owner's authoritative Backpack before settling the verified forge operation.

  2. Lines 3–13

    Pass the verified inputs, design hash, current forging level, and forged_item.key into the Backpack settlement routine.

  3. Lines 14–15

    Return the forge outcome after the Backpack record has captured the immutable ForgedItem PDA as item_pda.

Backpack stores the ForgedItem key and verified physical output

Rust programs/nicechunk_backpack/src/state.rs
        let mut outcome = calculate_forge_outcome(&materials, forging_level);
        if let Some(required) = requirements {
            outcome.volume_mm3 = required.required_volume_mm3.min(u32::MAX as u64) as u32;
            outcome.mass_grams = required.output_mass_grams;
        } else {
            outcome.mass_grams = materials
                .iter()
                .filter_map(|material| material.mass_grams().ok())
                .fold(0_u32, u32::saturating_add);
        }
        Self::remove_resources_at(data, owner, indexes, updated_slot)?;
        let mut output = BackpackSlotRecord {
            kind: BACKPACK_SLOT_KIND_ITEM,
            category: BACKPACK_ITEM_CATEGORY_FORGED,
            flags: 0,
            quantity: 1,
            resource: BackpackResourceRecord::default(),
            item_code: BACKPACK_FORGED_ITEM_CODE,
            item_id,
            item_pda: *item_pda,
            volume_mm3: outcome.volume_mm3,
            durability_current: outcome.durability_max,
            durability_max: outcome.durability_max,
            grade: outcome.grade,
            item_level: outcome.item_level,
            quality_bps: outcome.quality_bps,
            metadata: design_hash,
        };
        output.set_mass_grams(outcome.mass_grams)?;
        Self::append_item(data, owner, &output, updated_slot)?;

The record stores the unique ForgedItem PDA. Verified geometry replaces provisional volume and mass before consumption; item ID, durability, quality, and design hash remain separate fields.

  1. Lines 1–10

    Calculate the forge outcome, then replace provisional volume and mass with verified geometry requirements when those requirements are present.

  2. Lines 11–29

    Consume the selected materials and construct one forged Backpack record containing the unique ForgedItem PDA plus independent physical and quality fields.

  3. Lines 30–31

    Apply the verified mass encoding and append the complete output record to the owner's Backpack.

Appearance repacks visible identity and clears all rich equipment

Rust programs/nicechunk_player/src/state.rs
    pub fn pack(dst: &mut [u8], args: &PlayerAppearanceInitArgs) -> ProgramResult {
        if dst.len() != Self::LEN {
            return Err(NicechunkPlayerError::InvalidAppearanceData.into());
        }
        Self::validate_model_kind(args.model_kind)?;
        PlayerProfile::validate_name(args.display_name.as_bytes())?;
        Self::validate_title(args.title)?;
        Self::validate_model_code(args.model_code)?;

        let mut writer = ByteWriter { dst, offset: 0 };
        writer.bytes(&PLAYER_APPEARANCE_MAGIC)?;
        writer.u16(PLAYER_APPEARANCE_VERSION)?;
        writer.u8(args.bump)?;
        writer.u8(1)?;
        writer.pubkey(args.owner)?;
        writer.pubkey(args.player_profile)?;
        writer.pubkey(args.global_config)?;
        writer.pubkey(args.treasury_authority)?;
        writer.u8(args.model_kind)?;
        writer.u16(0)?;
        writer.u16(args.display_name.as_bytes().len() as u16)?;
        writer.u16(args.title.len() as u16)?;
        writer.u16(args.model_code.len() as u16)?;
        writer.u8(APPEARANCE_EQUIPMENT_SLOT_COUNT as u8)?;
        writer.u64(args.created_slot)?;
        writer.u64(args.updated_slot)?;
        writer.i64(args.created_at)?;
        writer.i64(args.updated_at)?;
        writer.bytes(&[0_u8; Self::HEADER_LEN - Self::RESERVED_OFFSET])?;

        let display_name_bytes = args.display_name.as_bytes();
        writer.bytes(display_name_bytes)?;
        writer.bytes(
            &[0_u8; PLAYER_NAME_MAX_BYTES][..PLAYER_NAME_MAX_BYTES - display_name_bytes.len()],
        )?;
        writer.bytes(args.title)?;
        writer.bytes(
            &[0_u8; APPEARANCE_TITLE_MAX_BYTES][..APPEARANCE_TITLE_MAX_BYTES - args.title.len()],
        )?;
        writer.bytes(args.model_code)?;
        writer.bytes(
            &[0_u8; APPEARANCE_MODEL_CODE_MAX_BYTES]
                [..APPEARANCE_MODEL_CODE_MAX_BYTES - args.model_code.len()],
        )?;
        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])?;
        }

The packer validates and rewrites the full visible identity record. Its final loop gives every rich equipment record state zero and a zero-filled payload.

  1. Lines 1–8

    Require the exact account size and validate model kind, name, title, and model-code envelope.

  2. Lines 10–29

    Write account relationships, lengths, timestamps, and the fixed header.

  3. Lines 31–44

    Write bounded display name, title, and model code with zero padding.

  4. Lines 45–49

    Reset every one of the twelve rich equipment records.

Player transfers a validated Backpack record into separate equipment custody

Rust programs/nicechunk_player/src/lib.rs
    } else {
        let backpack_record = {
            let backpack_data = backpack.try_borrow_data()?;
            BackpackAccountView::validate_pda_and_owner(
                &backpack_data,
                backpack.key,
                backpack.owner,
                authority.key,
            )?;
            BackpackAccountView::equipment_record_at(&backpack_data, authority.key, backpack_index)?
        };
        invoke_backpack_equipment_transfer(
            game_program,
            transfer_authority,
            authority,
            backpack,
            player_equipment,
            material_physics,
            &[1, 10, slot, backpack_index],
            transfer_bump,
        )?;
        {
            let mut equipment_data = player_equipment.try_borrow_mut_data()?;
            PlayerEquipment::write_custodied_slot(
                &mut equipment_data,
                slot,
                backpack_index,
                backpack.key,
                &backpack_record,
                &payload[4..],
                clock.slot,
            )?;
            next_identity = PlayerEquipment::slot_identity(&equipment_data, slot)?;
        }
    }

    let mut profile_data = player_profile.try_borrow_mut_data()?;
    PlayerProfile::write_equipment_slot(&mut profile_data, slot, &next_identity, clock.slot)

Player validates and removes the Backpack record, writes the complete record and model bytes into PlayerEquipment, and stores only the derived identity in PlayerProfile.

  1. Lines 2–11

    Validate the owner-derived Backpack and copy the requested equipment record before custody changes.

  2. Lines 12–21

    Invoke the unified Game Backpack transfer so the source record leaves Backpack custody atomically.

  3. Lines 22–35

    Write the full source record and model bytes into PlayerEquipment, then derive its compact slot identity.

  4. Lines 38–39

    Store only that derived identity in PlayerProfile after the custody record has been written.

Equipment-source Market validates custody and Profile identity

Rust programs/nicechunk_market/src/lib.rs
fn read_equipment_slot_for_listing(
    player_profile: &AccountInfo,
    player_equipment: &AccountInfo,
    owner: &Pubkey,
    equipment_slot: u8,
    player_program: &Pubkey,
    global_config: &Pubkey,
) -> Result<[u8; BACKPACK_SLOT_RECORD_LEN], solana_program::program_error::ProgramError> {
    require_key_eq(
        player_profile.owner,
        player_program,
        NicechunkMarketError::InvalidPlayerProgram,
    )?;
    require_key_eq(
        player_equipment.owner,
        player_program,
        NicechunkMarketError::InvalidPlayerProgram,
    )?;
    let profile_data = player_profile.try_borrow_data()?;
    validate_player_profile_data_and_pda(
        player_profile.key,
        &profile_data,
        owner,
        player_program,
        global_config,
    )?;
    if equipment_slot as usize >= PLAYER_PROFILE_EQUIPMENT_SLOT_COUNT {
        return Err(NicechunkMarketError::InvalidEquipmentSource.into());
    }
    let equipment_data = player_equipment.try_borrow_data()?;
    validate_player_equipment_data_and_pda(
        player_equipment.key,
        &equipment_data,
        owner,
        player_profile.key,
        player_program,
        global_config,
    )?;
    validate_equipment_source_record(&profile_data, &equipment_data, equipment_slot)
}

fn validate_equipment_source_record(
    profile_data: &[u8],
    equipment_data: &[u8],
    equipment_slot: u8,
) -> Result<[u8; BACKPACK_SLOT_RECORD_LEN], solana_program::program_error::ProgramError> {
    if profile_data.len() != PLAYER_PROFILE_LEN
        || equipment_data.len() != PLAYER_EQUIPMENT_LEN
        || equipment_slot as usize >= PLAYER_PROFILE_EQUIPMENT_SLOT_COUNT
    {
        return Err(NicechunkMarketError::InvalidEquipmentSource.into());
    }
    let offset =
        PLAYER_EQUIPMENT_SLOTS_OFFSET + equipment_slot as usize * PLAYER_EQUIPMENT_SLOT_LEN;
    if equipment_data[offset + PLAYER_EQUIPMENT_RECORD_STATE_OFFSET] != 1
        || equipment_data[offset + PLAYER_EQUIPMENT_RECORD_SLOT_OFFSET] != equipment_slot
        || equipment_data[offset + PLAYER_EQUIPMENT_RECORD_FLAGS_OFFSET]
            & PLAYER_EQUIPMENT_FLAG_CUSTODY
            == 0
    {
        return Err(NicechunkMarketError::InvalidEquipmentSource.into());
    }
    let source_slot = copy_valid_backpack_slot(
        &equipment_data[offset + PLAYER_EQUIPMENT_RECORD_BACKPACK_SLOT_OFFSET
            ..offset + PLAYER_EQUIPMENT_RECORD_BACKPACK_SLOT_OFFSET + BACKPACK_SLOT_RECORD_LEN],
    )?;
    let identity = Pubkey::new_from_array(
        hashv(&[
            b"equipment-v2",
            &equipment_data[offset + PLAYER_EQUIPMENT_RECORD_BACKPACK_OFFSET
                ..offset + PLAYER_EQUIPMENT_RECORD_BACKPACK_OFFSET + 32],
            &source_slot,
        ])
        .to_bytes(),
    );
    let profile_offset = PLAYER_PROFILE_EQUIPMENT_OFFSET + equipment_slot as usize * 32;
    if &profile_data[profile_offset..profile_offset + 32] != identity.as_ref() {
        return Err(NicechunkMarketError::InvalidEquipmentSource.into());
    }
    Ok(source_slot)
}

Market validates canonical Profile and PlayerEquipment PDAs, requires custody, reconstructs the identity from the full source record, and requires PlayerProfile to contain that identity.

  1. Lines 1–37

    Require both accounts to belong to Player Program and validate their canonical owner, Profile, and GlobalConfig relationships.

  2. Lines 39–57

    Require exact account sizes, a valid slot index, initialized slot state, matching slot number, and the custody flag.

  3. Lines 58–72

    Copy the embedded Backpack source record and reconstruct its equipment-v2 identity from Backpack address plus record bytes.

  4. Lines 73–78

    Reject the listing source unless PlayerProfile stores that reconstructed identity in the same equipment slot.

The browser derives Chunk and Smelting progress under different programs

JavaScript src/chain/nicechunkChain.js
function derivePlayerProgressPdaForContext(owner, context = gameContext) {
  return derivePlayerProgressPdaForProgram(owner, context.chunkProgramId);
}

function deriveSmeltingPlayerProgressPdaForContext(owner, context = gameContext) {
  return derivePlayerProgressPdaForProgram(owner, context.smeltingProgramId);
}

The seed helper is shared, but context.chunkProgramId and context.smeltingProgramId place the addresses in different program domains.

  1. Lines 1–3

    Derive the progress address under Chunk.

  2. Lines 5–7

    Derive the Smelting progress address under its configured program, currently unified Game.

Current refresh batches all three chain skill domains

JavaScript src/chain/nicechunkChain.js
export async function fetchPlayerProgress(ownerAddress, {
  connection: connectionOverride = null,
  context = gameContext,
} = {}) {
  if (!ownerAddress) return null;
  const owner = typeof ownerAddress === "string" ? new PublicKey(ownerAddress) : ownerAddress;
  const globalConfig = deriveGlobalConfigPda();
  const [chunkProgress, chunkBump] = derivePlayerProgressPdaForContext(owner, context);
  const [smeltingProgress, smeltingBump] = deriveSmeltingPlayerProgressPdaForContext(owner, context);
  const [playerSkills, skillsBump] = derivePlayerSkillsPda(owner);
  const conn = connectionOverride ?? getNicechunkConnection();
  const [accounts, initialRuleTable] = await Promise.all([
    conn.getMultipleAccountsInfo([chunkProgress, smeltingProgress, playerSkills], "confirmed"),
    fetchSkillRuleTable({ connection: conn }),
  ]);
  const [chunkAccount, smeltingAccount, skillsAccount] = accounts;
  const chunkDomain = decodePlayerProgressDomain({
    account: chunkAccount,
    expectedProgramId: context.chunkProgramId,
    expectedBump: chunkBump,
    owner,
    globalConfig,
  });
  const smeltingDomain = decodePlayerProgressDomain({
    account: smeltingAccount,
    expectedProgramId: context.smeltingProgramId,
    expectedBump: smeltingBump,
    owner,
    globalConfig,
  });
  const skillsDomain = decodePlayerSkillsDomain({
    account: skillsAccount,
    expectedBump: skillsBump,
    owner,
    globalConfig,
  });
  const skillRuleTable = skillsDomain && initialRuleTable.revision !== skillsDomain.ruleRevision
    ? await fetchSkillRuleTable({ connection: conn, force: true })
    : initialRuleTable;
  const emptySkills = Object.fromEntries(PLAYER_SKILL_IDS.map((skillId) => [skillId, 0]));
  return {
    publicKey: chunkProgress.toBase58(),
    smeltingPublicKey: smeltingProgress.toBase58(),
    owner: owner.toBase58(),
    precisionGatheringXp: chunkDomain?.precisionGatheringXp ?? 0,
    smeltingXp: smeltingDomain?.smeltingXp ?? 0,
    explorationXp: chunkDomain?.explorationXp ?? 0,
    playerSkillsPublicKey: playerSkills.toBase58(),
    playerSkillsInitialized: Boolean(skillsDomain),
    skillXp: skillsDomain?.xp ?? emptySkills,
    skillLevels: skillsDomain?.levels ?? emptySkills,
    skillCursorMask: skillsDomain?.cursorMask ?? 0,
    skillThresholds: skillRuleTable.thresholds,
    skillRuleRevision: skillsDomain?.ruleRevision ?? 0,
    skillRuleTableRevision: skillRuleTable.revision,
    skillRulesCurrent: Boolean(skillsDomain && skillsDomain.ruleRevision === skillRuleTable.revision),
    programId: context.chunkProgramId.toBase58(),
    smeltingProgramId: context.smeltingProgramId.toBase58(),
    skillsProgramId: NICECHUNK_SKILLS_PROGRAM_ID.toBase58(),
  };
}

The helper fetches Chunk Progress, Smelting Progress, and PlayerSkills, validates every domain, refreshes mismatched SkillRuleTable data, and returns chain XP, levels, and thresholds.

  1. Lines 1–13

    Derive the three owner-scoped progress accounts and fetch them with the current SkillRuleTable at confirmed commitment.

  2. Lines 14–34

    Decode Chunk, Smelting, and Skills domains against their expected programs, bumps, owner, and GlobalConfig.

  3. Lines 35–39

    Force-refresh the rule table when the PlayerSkills revision differs from the initially fetched revision.

  4. Lines 40–59

    Return chain-derived XP, levels, thresholds, revision status, and the three authoritative program identifiers.

Profile skill state accepts chain values only

JavaScript play/play-profile-skills.js
export function buildProfileSkillState({
  chainXp = null,
  chainLevels = null,
  chainThresholds = null,
} = {}) {
  const levels = normalizeSkillLevels(chainLevels);
  const xpBySkill = normalizeSkillXp(chainXp);
  const thresholdsBySkill = normalizeSkillThresholds(chainThresholds);
  return {
    levels,
    xpBySkill,
    thresholdsBySkill,
    resolvedLevels: { ...levels },
    source: "chain",
  };
}

The profile normalizes XP, levels, and thresholds from chain fetches. It no longer reads or max-merges localStorage skill history.

  1. Lines 1–8

    Normalize only the supplied chain XP, levels, and threshold maps into complete per-skill values.

  2. Lines 9–14

    Expose those normalized levels as both displayed and resolved values and label the source explicitly as chain.

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 current Profile, PlayerEquipment, Appearance, Session, UsernameIndex, InviteIndex, and Backpack-view layouts; equipment-v2 identity; model validation; name rules; fixed attributes; and zero-filled rich Appearance equipment.

programs/nicechunk_player/src/lib.rs

Implements signed Profile writes, Y-only position bounds, name-index lifecycle, Appearance upsert and treasury close, final tag 13 and 14 equipment custody operations, tag 15 durability, tag 16 Market release, and forging-result receipt.

programs/nicechunk_player/src/cluster_config.rs

Pins the Core, standalone Backpack, unified Game, treasury, and related program identities accepted by the Player build.

programs/nicechunk_backpack/src/lib.rs

Creates the unique forged-item-v1 PDA, stores its verified NCF1 account, passes forged_item.key into the Backpack output, transfers Backpack and PlayerEquipment custody, and sends the forge outcome into Player.

programs/nicechunk_backpack/src/state.rs

Defines immutable ForgedItem storage, complete forged Backpack records, outcome math, one XP per completed forge, durability, dense move or replacement operations, and the PlayerEquipment custody view.

programs/nicechunk_market/src/lib.rs

Validates exact Backpack or PlayerEquipment custody sources, reconstructs equipment-v2 from the selected complete record, requires the corresponding Profile identity, and moves one source into Listing atomically.

programs/nicechunk_market/src/cluster_config.rs

Selects the one Backpack program identity accepted by a particular Market build.

programs/nicechunk_game/Cargo.toml

Shows the unified Game feature composition used by the current Backpack, Smelting, and Market namespaces.

programs/nicechunk_chunk/src/state.rs

Defines Chunk's 128-byte PlayerProgress layout, Precision and Exploration fields, thresholds, and explored-chunk count.

programs/nicechunk_chunk/src/lib.rs

Creates and mutates Chunk-scoped PlayerProgress during verified world actions.

programs/nicechunk_smelting/src/state.rs

Defines the identically seeded Smelting PlayerProgress layout and Smelting threshold rules under the Smelting program domain.

programs/nicechunk_smelting/src/lib.rs

Creates or updates Smelting-scoped progress during successful settlement.

src/chain/nicechunkChain.js

Batches Chunk Progress, Smelting Progress, PlayerSkills, and SkillRuleTable; derives and decodes PlayerEquipment and ForgedItem; decodes Profile identities; performs cache-first Backpack loading; and builds current name instructions.

play/play-chain-player.js

Refreshes Profile, PlayerEquipment, Appearance, and the aggregate progress object together; exposes PlayerSkills maps to the UI; reconciles chain hotbar custody; and constructs forced resource-mine checkpoints.

play/play-chain-adapter.js

Marks verified resource mining as the current action class eligible to carry or trigger a chain Profile-position checkpoint.

play/main.js

Chooses Play startup independently of chain Profile position and requests a supplemental Profile checkpoint after confirmed resource mining when needed.

play/position-persistence.js

Stores ordinary Play position locally, separately from chain Profile checkpoints.

play/play-profile-skills.js

Defines ten visible skills and effects, then builds displayed Profile skill state exclusively from supplied chain XP, levels, and SkillRuleTable thresholds.

play/play-profile-ui.js

Builds Profile level, reputation, region, stats, selected-hotbar hand, synchronized Backpack, and empty or locked conceptual equipment slots without rendering Profile.equipment.

play/play-chain-backpack.js

Supplies the synchronized Backpack summary used by Profile UI after the chain module's cache-first load.

play/play-profile-session.js

Defines current visible identity and avatar fallback precedence between chain Appearance, local profile, and built-in villagers.

chunk.js/renderer/avatar-mesh.js

Resolves built-in avatar aliases and renders both NCM2 and NCM4, so a prefix-only chain-accepted modelCode can still fail mesh construction.

sdk/nicechunk-player.ts

Contains older name limits and account lists that do not yet match the current processor requirements for nonempty name update and Appearance upsert.

tests/nicechunk_player_chunk.ts

Asserts older SDK account construction, which explains why those passing tests do not by themselves prove deployed name-index parity.

public/mainnet.json

Declares the website's current Devnet cluster and Player, Chunk, Backpack, Smelting, Market, and Game program configuration.

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

Retains the confirmed slot-476,758,441 raw Player Program account bytes, hashes, classifications, current-versus-earlier Profile derivation result, live field summary, ProgramData length split, and local ELF comparison.

docs/visual-provenance/player-profile.json

Records reference images, exact imagegen prompts, generator calls, source and output hashes, crop decisions, review boundaries, and the superseded unprovenanced hero.