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.
One current player-v7 PDA under the Player Program.
A separate appearance-v1 PDA under the Player Program.
The same progress seed tuple remains program-scoped.
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.
The current Player instruction set has no write path for these six u16 values.
Only Y is contract-bounded; current chain saves are tied to the verified resource-mine path.
Current Play chooses a query pose, local saved position, or default start.
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.
- 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.
- 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.
- 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.
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.
forged-item-v1, creator, and item ID derive the immutable model account.
The selected record leaves or exchanges with Backpack atomically.
Market checks this identity against the selected custodied slot.
The equipment identity becomes stale until another identity-writing operation.
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.
All ten Profile skill XP and level values come from the validated chain account.
A revision mismatch triggers one forced table refresh before return.
Legacy Progress remains available for validation and synchronization, not UI max-merging.
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.
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.
Raw account bytes and hashes are retained in the dedicated confirmed snapshot.
Four earlier records are also 773 bytes, so address derivation matters.
The smaller number excludes 45 bytes of loader metadata.
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 bytesProfile 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 || builtInVillagerThe 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 forgeNo 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
Rustprograms/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.
- Lines 1–2
Open a scoped mutable borrow of the owner's authoritative Backpack before settling the verified forge operation.
- Lines 3–13
Pass the verified inputs, design hash, current forging level, and forged_item.key into the Backpack settlement routine.
- 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
Rustprograms/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.
- Lines 1–10
Calculate the forge outcome, then replace provisional volume and mass with verified geometry requirements when those requirements are present.
- Lines 11–29
Consume the selected materials and construct one forged Backpack record containing the unique ForgedItem PDA plus independent physical and quality fields.
- 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
Rustprograms/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.
- Lines 1–8
Require the exact account size and validate model kind, name, title, and model-code envelope.
- Lines 10–29
Write account relationships, lengths, timestamps, and the fixed header.
- Lines 31–44
Write bounded display name, title, and model code with zero padding.
- Lines 45–49
Reset every one of the twelve rich equipment records.
Player transfers a validated Backpack record into separate equipment custody
Rustprograms/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.
- Lines 2–11
Validate the owner-derived Backpack and copy the requested equipment record before custody changes.
- Lines 12–21
Invoke the unified Game Backpack transfer so the source record leaves Backpack custody atomically.
- Lines 22–35
Write the full source record and model bytes into PlayerEquipment, then derive its compact slot identity.
- 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
Rustprograms/nicechunk_market/src/lib.rsfn 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.
- Lines 1–37
Require both accounts to belong to Player Program and validate their canonical owner, Profile, and GlobalConfig relationships.
- Lines 39–57
Require exact account sizes, a valid slot index, initialized slot state, matching slot number, and the custody flag.
- Lines 58–72
Copy the embedded Backpack source record and reconstruct its equipment-v2 identity from Backpack address plus record bytes.
- 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
JavaScriptsrc/chain/nicechunkChain.jsfunction 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.
- Lines 1–3
Derive the progress address under Chunk.
- Lines 5–7
Derive the Smelting progress address under its configured program, currently unified Game.
Current refresh batches all three chain skill domains
JavaScriptsrc/chain/nicechunkChain.jsexport 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.
- Lines 1–13
Derive the three owner-scoped progress accounts and fetch them with the current SkillRuleTable at confirmed commitment.
- Lines 14–34
Decode Chunk, Smelting, and Skills domains against their expected programs, bumps, owner, and GlobalConfig.
- Lines 35–39
Force-refresh the rule table when the PlayerSkills revision differs from the initially fetched revision.
- Lines 40–59
Return chain-derived XP, levels, thresholds, revision status, and the three authoritative program identifiers.
Profile skill state accepts chain values only
JavaScriptplay/play-profile-skills.jsexport 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.
- Lines 1–8
Normalize only the supplied chain XP, levels, and threshold maps into complete per-skill values.
- 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.