PLAYER, WALLET, AND CONTROL · STEP 8

Equipment and Custody: The Record Moves With the Tool

Equipping a forged item transfers its complete record from Backpack custody into one of nine PlayerEquipment slots and publishes that slot's derived identity through PlayerProfile. The visible and usable tool depends on the custodied record and hash-validated NCF1 bytes preserved on chain. A Market listing can snapshot either a Backpack record or a custodied PlayerEquipment record before atomically removing it from its source. This guide follows production /play/ and clearly separates current source from dated Devnet history.

37 min read
Generated voxel village workshop where the canonical girl stands beside a forged tool in an open Backpack, the canonical boy places a cyan reference tile into a multi-cell wall equipment rack, and a rough unpainted tool rests on a local rehearsal stand.
Read the open Backpack as the source immediately before transfer, the rack as PlayerEquipment custody, and the small tile as the derived identity published by PlayerProfile. The rough model represents verified NCF1 reconstruction. This generated scene uses the canonical villagers and real Chunk.js scene and baked-material references, but every prop and gesture is a teaching metaphor rather than decoded account data or transaction proof.
PLAYER QUESTION When I equip, hold, list, cancel, or buy a forged tool, where is the actual item record—and which part of what I see is really on chain?
Learning path
Player, wallet, and control · Step 8 of 15
Audience
Players with no programming or blockchain background
Implemented boundary
Current Player, Backpack, Market, Chunk, production Play, Guardian, NCF1, and renderer behavior · separate pre-architecture Devnet observation
Reviewed
30 July 2026 · literal current-source excerpts, focused equipment tests, generated visual provenance, and clearly dated slot-477,030,223 historical evidence
Wallet authority
The public key whose wallet must approve a transaction. The private signing key must never be placed in a Profile, Backpack, Listing, browser message, or support chat.
Solana account owner
The program ID allowed by Solana to change an account's data. It is not the same as the player wallet written inside that data.
Stored player owner
A wallet public key inside Profile or Backpack bytes that a program checks to establish the player relationship.
PlayerProfile
The compact public player account that stores the wallet relationship, one GlobalConfig address, nine equipment references, one selected Backpack reference, and progression fields. It does not contain full tool records or model geometry.
GlobalConfig
A shared configuration account whose address is stored in PlayerProfile and checked by Player, Market, and Chunk flows so unrelated account families cannot be mixed silently.
Custody
The current program-controlled location of the complete 80-byte inventory record: Backpack, one custodied PlayerEquipment slot, an active Listing snapshot, or a destination Backpack after cancel or purchase.
Backpack
A public inventory account with a header and a dense array of block or item records. Equipping removes or exchanges the selected record atomically; unequipping appends the custodied record back.
Dense record array
A list with no empty gap between active records. Removing one record shifts later records toward the front, so a Backpack index is not permanent identity.
Record index
A zero-based position in the current Backpack array. It differs from a Profile equipment slot, item ID, design hash, and PDA.
PDA (program-derived address)
A public account address deterministically derived from a program ID and seed bytes, with no private signing key. A field named item_pda does not by itself prove that a unique item account exists there.
item_pda
A 32-byte field in an item record. For a current forged output it is the unique immutable ForgedItem PDA derived from the creator wallet and item ID; it is not the value published in PlayerProfile when the item is equipped.
PlayerEquipment
A 7,040-byte Player Program PDA derived from player-equipment-v1 and the owner wallet, with a 128-byte header and nine 768-byte slots. Each active slot stores source Backpack identity, the complete 80-byte record, custody flags, and up to 640 verified NCF1 bytes.
Equipment slot identity
A 32-byte hashv digest of the literal tag equipment-v2, the source Backpack address, and the complete stored 80-byte record. PlayerProfile publishes this derived value; it does not publish item_pda.
Equipment slot
One of nine numeric positions shared by PlayerEquipment and PlayerProfile. The contract enforces indexes 0 through 8 but assigns no anatomical or item-category meaning; production Play uses them as its chain-backed hotbar positions.
Clear sentinel
Backpack index 255 in Player tag 13. It requests unequip: return the custodied record to the supplied owner Backpack, clear the PlayerEquipment slot, and publish the all-zero Profile identity.
Forged record
An 80-byte Backpack item record carrying identifiers, item_pda, volume, durability, grade, level, quality, and a 32-bit metadata value used for the design hash.
Design hash
The 32-bit FNV-1a fingerprint stored in the record metadata and ForgedItem account. It verifies the supplied NCF1 bytes within this format but is not a collision-resistant global content identity.
NCF1
The compact forged-object byte format used to reconstruct a tool. Current forging stores the complete 14-to-640-byte version-15 payload in the immutable ForgedItem PDA; equipping also stores the verified payload in PlayerEquipment for fresh-client reconstruction.
Hotbar
The browser's current quick-selection bar. It controls what the local avatar presents and can differ from the nine chain Profile references.
AABB collision
A simple box-to-box contact test whose sides stay aligned with the world axes. Production mining samples tool boxes during a swing and also sweeps between samples so fast motion cannot jump through a block unnoticed.
Durability settlement
The browser tentatively subtracts local hotbar wear during a swing. When a block break is submitted, production includes Player tag 15 instructions for custodied forged tools in the same transaction; failure restores the tentative local wear, while success leaves the reduced durability in PlayerEquipment.
Guardian equipment message
In production Play, a realtime presentation message carrying right-hand kind and variant, a forged flag, design hash, and optional hash-validated NCF1 bytes. It carries neither Profile equipment keys nor Backpack presentation fields and is not a wallet signature or custody certificate.
Rich equipment
Twelve 576-byte records reserved inside PlayerAppearance. Current Appearance packing resets them to empty, so they are not the active nine-slot equip path.
Listing
A 216-byte Market account that can hold a complete 80-byte source record plus seller, price, source, timestamps, state, and buyer history.
Atomic transaction
A Solana transaction whose instructions and program calls all succeed or all roll back. A failed buyer-Backpack append therefore prevents payment settlement in the same purchase transaction.
Local storage
Browser data saved on one device. It may cache hotbar presentation, but current forged geometry is recoverable from validated ForgedItem or PlayerEquipment account data and does not depend on that cache for canonical persistence.
Dated snapshot
Account bytes observed through RPC at one confirmed context slot. It can show state at that moment but cannot prove that the reviewed source exactly produced the deployed program binary.

Key points

Equip transfers custody instead of copying a shortcut

Player tag 13 validates nine accounts, moves the selected complete record out of the unified-Game Backpack, writes it and any verified model bytes into PlayerEquipment, and publishes a derived equipment-v2 identity through PlayerProfile.

Every current forged output has one immutable model account

Forging derives ForgedItem from forged-item-v1, creator, and item ID, stores the complete verified NCF1 payload in that 752-byte account, and writes its unique address into the Backpack record's item_pda field.

Local wear previews an atomic chain durability update

Production Play records per-slot wear while a swing is pending, converts custodied forged wear into Player tag 15 instructions, and submits those instructions with mining. A failed transaction restores local wear; a successful transaction reduces the PlayerEquipment record on chain.

Market listing accepts either custody source

Backpack source snapshots the complete 80-byte record and densely removes it from the Backpack. Equipment source snapshots a validated custodied PlayerEquipment slot, then atomically clears that PlayerEquipment slot and its published Profile identity.

Production Play writes and reloads authoritative equipment

Production /play/ batches hotbar edits through setPlayerEquipmentSlotsOnChain, reloads PlayerEquipment and affected Backpacks after success, and rolls the local mutation back when submission fails. Market source selection likewise comes from validated chain custody.

ONE WORD, FOUR DIFFERENT QUESTIONS

Ownership does not tell you where the item record is or what appears in your hand

Players often say I own this tool, but the implementation needs four separate statements. First, a Solana account has a program owner: only that program may directly change its bytes. Second, Profile, Backpack, and Listing data can store a player wallet or seller public key. Third, the complete usable record has a current location. Fourth, the game on one device has a current presentation choice. These statements can agree, but none automatically proves the others.

A current production Backpack is owned at the Solana level by the unified Game Program while its data names Alice's wallet as the player owner. A forged record inside it is Alice's inventory even when no Profile equipment slot refers to it. Once equipped, that exact record leaves Backpack and enters Alice's owner-derived PlayerEquipment account; PlayerProfile retains only the derived equipment identity.

Wallet signatures authorize supported changes; they do not encrypt public account bytes. Program ownership governs writes; it does not mean the program possesses a human wallet. A local mesh proves only that the browser reconstructed and displayed geometry. Use the exact phrase that answers the exact question: account owner, stored owner, record location, or screen presentation.

The current transfer instruction requires the supplied Backpack to be owned by the configured unified Game Program and the PlayerEquipment account to be owned by the Player Program. The separate program owner checks, owner-derived PDAs, stored wallet owner, PlayerProfile address, and GlobalConfig binding all have to agree. Program domain therefore matters independently of the wallet stored inside an account.

Write authority Solana account.owner

The program allowed to change the account data.

Player relationship Wallet key stored in data

The value processors compare with the signing or destination player.

Custody location Backpack, PlayerEquipment, or Listing

The one authoritative location of the complete 80-byte record.

Visible selection Local hotbar and reconstructed mesh

What one browser currently presents in the avatar's hand.

Generated voxel workshop where the canonical girl holds a small wooden owner plug beside a forged tool in an open Backpack while the canonical boy examines a rough unpainted tool on a stand near a program-controlled wall recess.
The wall recess, owner plug, open Backpack with its complete record, and rough local tool stand separate write authority, stored player relationship, custody, and presentation. They are generated teaching metaphors built from the canonical villagers and real Chunk.js environmental references; their shapes, poses, and placement are not program IDs, signatures, account bytes, or proof of ownership.

SIGNED, ATOMIC CUSTODY TRANSFER

Equipping moves the complete record into PlayerEquipment

Player tag 13 carries a slot from 0 through 8, a Backpack index, a little-endian model length, and the model bytes. It requires nine accounts: writable signing wallet authority, writable current PlayerProfile, writable owner-derived PlayerEquipment, GlobalConfig, MaterialPhysics, System Program, writable owner Backpack, executable unified Game Program, and the Player Program's equipment-transfer authority PDA. PlayerEquipment is created lazily at its final 7,040-byte layout when it does not yet exist.

For an equip request, Player validates the Backpack's current version, owner, ID-derived PDA, dense index, and mass-valid state, then copies the selected complete 80-byte record before invoking the Game Program. A block needs nonzero quantity. An item additionally needs nonzero item ID and item_pda. The current transfer path is not forged-only: blocks and valid item records can occupy the numeric hotbar slots. Non-forged records must carry no model bytes; a forged item requires 14 to 640 NCF1 bytes whose version-15 nibble and FNV-1a hash match its record metadata.

The Game Program performs the custody move. If the target PlayerEquipment slot was empty, it densely removes the selected Backpack record. If that equipment slot already held a record, Game replaces the selected Backpack position with the old custodied record, effectively exchanging the two records without changing Backpack item count. Player then writes the selected record, source Backpack address, former index, custody flags, and verified model bytes into the 768-byte equipment slot.

PlayerProfile does not receive item_pda. It receives hashv(equipment-v2, source Backpack address, complete 80-byte record), represented as a 32-byte public key. This identity lets later Market release verify that Profile and PlayerEquipment describe the same custodied bytes. The contract still gives slot numbers no anatomical meaning; production Play maps the same nine indexes to its chain-backed hotbar.

Backpack index 255 requests unequip and requires zero model bytes. The same account set is still supplied because the Game Program must append the custodied record to the chosen owner Backpack after validating material mass. Only after that succeeds does Player clear the 768-byte slot and publish the zero Profile identity. A full or invalid destination Backpack makes the whole transaction fail, so clearing cannot silently destroy the record.

Production Play reaches these instructions through play/play-chain-player.js and src/chain/nicechunkChain.js. It swaps already-custodied records first with Player tag 14, resolves each incoming Backpack item against a fresh account read because dense removal changes later indexes, processes clears last, then reloads PlayerEquipment and every affected Backpack. Failed submission restores the pending local hotbar mutation.

  1. Validate the account family

    Player checks the wallet signer, current Profile, owner-derived PlayerEquipment, GlobalConfig, unified Game Program, transfer-authority PDA, Backpack PDA, and MaterialPhysics account.

  2. Resolve one complete source record

    The selected dense record is copied before mutation; forged model bytes must independently pass size, version, and design-hash validation.

  3. Move or exchange custody

    An empty equipment target densely removes the source; an occupied target writes its old record back at the selected Backpack index.

  4. Publish the derived identity

    PlayerEquipment stores record and model bytes, while PlayerProfile stores only the equipment-v2 hash identity used for cross-account consistency.

  5. Index 255 returns custody

    Unequip appends the record to a valid owner Backpack before clearing PlayerEquipment and the corresponding Profile identity.

Generated voxel workshop showing the source moment before a complete forged tool leaves an open Backpack for a multi-cell equipment rack while a small cyan identity tile is prepared beside it.
Treat the open Backpack as the validated source before the atomic move, the rack as PlayerEquipment custody, and the small tile as the derived Profile identity. The still image cannot show the subsequent Backpack removal, so the exact move-or-exchange behavior, slot count, account order, and rollback guarantees come from the audited source rather than the prop positions.

ONE OUTPUT, ONE IMMUTABLE MODEL ACCOUNT

Every current forged tool receives a unique ForgedItem PDA

A verified forge accepts canonical version-15 NCF1 bytes, validates the encoded design and material requirements, and derives a ForgedItem PDA from the literal seed forged-item-v1, the creator wallet, and the new eight-byte item ID. The address must still be an empty System Program account. Reusing the same creator and item ID is rejected once the PDA has been initialized.

Backpack creates that 752-byte program-owned account and writes version 1, item ID, creator, origin Backpack, 32-bit design hash, code length, the complete 14-to-640-byte NCF1 payload, creation slot, and creation time. It then consumes the selected materials and appends an 80-byte forged Backpack record whose item_pda is this ForgedItem address. Forging XP is invoked afterward. Account creation, material consumption, output append, and XP share the same Solana transaction, so any later failure rolls every earlier write back.

The ForgedItem account is the canonical persistent model source for that output. Current code exposes creation and validation but no instruction that edits an initialized ForgedItem. Production Backpack and Market loaders batch-fetch item_pda accounts, require the configured Backpack/Game Program owner, validate magic, version, initialized state, creator-derived PDA, item ID, NCF1 bounds and hash, then require the decoded item ID and design hash to match the containing 80-byte record. Invalid accounts are ignored and cannot supply an equippable model.

When the owner equips the forged record, production passes those validated model bytes into Player tag 13. PlayerEquipment repeats the 14-to-640-byte, version-15, and FNV-1a checks before storing the NCF1 beside the custodied record. A fresh client can therefore reconstruct a forged tool from ForgedItem while it is in Backpack or Listing custody, and directly from PlayerEquipment while it is equipped; browser local storage is only a cache, not the canonical model archive.

The design hash still has only 4,294,967,296 possible values and is not collision-resistant. Global uniqueness comes from the PDA seeds and item ID, not from pretending a 32-bit checksum is a cryptographic content address. Market equipment custody independently hashes the source Backpack address and full record into the equipment-v2 Profile identity, so item_pda, design hash, equipment identity, and hotbar index remain four different identifiers.

ForgedItem PDA seeds forged-item-v1 · creator · item ID

The eight-byte item ID makes each successful output address unique for its creator.

Immutable model account 752 bytes

A 96-byte header, space for up to 640 NCF1 bytes, then creation slot and time.

Inventory record 80 bytes

Carries the ForgedItem PDA, outcome values, and design hash while custody changes.

Canonical geometry NCF1 v15 · 14–640 bytes

Persisted in ForgedItem and copied into PlayerEquipment after independent validation.

Generated voxel forge where the canonical villagers compare two visibly different forged tools, each paired with its own mosaic design board and account notebook while matching cyan markers indicate a shared account format.
The two tool-and-board pairs represent separate forged outputs with separate item IDs, ForgedItem PDAs, and canonical NCF1 payloads. Read the matching cyan markers only as a shared account type, never as equal addresses. Tool shapes, boards, notebooks, dials, and lighting are generated metaphors; exact hashes, bytes, and PDA derivations come from source.

CUSTODY, PRESENTATION, COLLISION, AND WEAR

The visible tool comes from chain-backed geometry, while interaction still begins locally

Six related structures serve different jobs. PlayerProfile publishes nine derived identities. PlayerEquipment is the authoritative nine-slot custody and model account. ForgedItem preserves one output's canonical NCF1 while the item is elsewhere. PlayerAppearance reserves twelve rich-equipment records, but current packing leaves them empty. The browser hotbar presents and animates the selected chain-backed slot. Guardian relays nearby presentation. Calling all six simply equipped would hide the checks that actually matter.

Production Play fetches PlayerEquipment, converts each custodied record and stored model payload into the matching hotbar position, and refuses to invent a forged tool when the record or NCF1 cannot be reconstructed. The verified geometry and grip determine tool placement and collision parts. Target selection first needs the geometry-derived reach sphere to intersect the block; sampled and swept AABB tests must then find contact along the animated swing path.

play/play-guardian.js sends only right-hand kind and variant, a forged flag, design hash, and optional NCF1 bytes after version, size, and hash validation. A receiver applies the same validation. Guardian does not transmit PlayerEquipment custody, Profile identity, item_pda, wallet signatures, or an RPC freshness proof. It is a realtime stage direction for nearby avatars, not a deed to the prop.

Every physical hit tentatively subtracts one point from the selected browser hotbar and records damage by local slot. Once the block break becomes submit-ready, mining-controller maps only forged tools whose custodySource is equipment into equipment-slot damage. The chain adapter passes that list as options.durabilityDamage. recordBlockBreakOnChain, tree-felling, and the primary support-collapse transaction each append one Player tag 15 instruction per normalized equipment slot.

Player tag 15 requires the session authority signer, canonical owner Profile, active break-block PlayerSession, owner-derived PlayerEquipment, matching GlobalConfig, a custodied forged record with item code 8, and a positive amount no greater than current durability. It subtracts from the 80-byte record inside PlayerEquipment in the same transaction as mining. If any mining or durability instruction fails, chain state rolls back and the pending local wear is restored; success leaves the chain record reduced. The legacy toolSlot argument remains unused, because the authoritative selector is the explicit equipmentSlot carried in durabilityDamage.

One current consistency edge must be stated plainly. equipment-v2 identity hashes the complete 80-byte record, including mutable current durability, but tag 15 changes only PlayerEquipment and does not rewrite PlayerProfile. After successful wear, the published Profile identity is therefore stale. Mining and unequip do not compare that identity, but direct equipment-source Market release does and will reject the worn slot until another operation republishes the identity—for example, unequip and re-equip, or a slot swap. This is current implemented behavior, not a promise that the stale identity is desirable.

  • Profile equipment: nine equipment-v2 identities, not item_pda values or full records.
  • PlayerEquipment: nine custodied records with source Backpack data and optional verified model bytes.
  • ForgedItem: one immutable canonical NCF1 account per current forged output.
  • Appearance rich equipment: twelve reserved records that current packing leaves empty.
  • Local hotbar: chain-backed presentation plus tentative interaction state while a transaction is pending.
  • Production Guardian: held kind and variant, forged flag, design hash, and optional validated NCF1—not custody proof.
  • Mining collision: locally restored geometry must pass reach-sphere plus sampled and swept AABB checks.
  • Durability settlement: Player tag 15 mutates the custodied chain record atomically with a successful mine.
  • Identity edge: durability currently changes the hashed record without refreshing the matching Profile identity.
Generated voxel riverside game scene where the canonical girl swings a forged tool at stone while the canonical boy stands between an open Backpack, two empty equipment racks, and a small local rehearsal stand.
The held tool represents local animation and collision, the rack represents PlayerEquipment custody, and the open Backpack represents a different custody source. This generated scene uses canonical villagers and real Chunk.js references, but it does not prove a particular hotbar slot, Guardian message, chain durability update, Profile identity, or account freshness.

FULL-RECORD ESCROW-LIKE STORAGE

Listing moves the complete record from its current custody into the Market

Creating a current Market listing begins with seller, new Listing, and System Program accounts, then consumes source-specific accounts. Backpack source validates a seller-owned Backpack record and the configured Backpack/Game Program. Equipment source instead validates PlayerProfile, owner-derived PlayerEquipment, GlobalConfig, and Player Program, then reads the complete record from the selected custodied slot and verifies that its current equipment-v2 identity matches the published Profile slot. Both paths require the seller's existing MarketUser PDA and reject non-transferable blueprints.

After the Listing stores the complete 80-byte source record, Backpack source invokes dense removal: every later record shifts one position toward zero, the old last slot is cleared, and item_count decreases. Equipment source invokes Player to validate the newly created Listing, clear the selected PlayerEquipment custody slot, and clear the matching Profile identity. These source transitions share the listing transaction, so any failed validation or CPI rolls back the Listing and custody changes together.

The Listing is 216 bytes and stores the complete 80-byte source record, including a forged item's immutable ForgedItem address. Clients can hydrate that model account while the item is listed; Listing does not duplicate the NCF1 bytes. During cancellation, current Market requires seller, Listing, a seller-owned destination Backpack, Backpack Program, Market authority, MaterialPhysics, GlobalConfig, and the seller's MarketUser PDA. It appends the saved record, marks the Listing canceled, and decrements the active-listing count. The Listing account and copied record remain as settlement history, but they are no longer active custody after the append succeeds.

During purchase, the same transaction validates currency accounts, transfers SOL or NCK shares, appends the saved record to the buyer's supplied Backpack through Market authority, and marks the Listing sold with buyer and time. Solana transaction atomicity means a failed Backpack append or later program error rolls back earlier payment changes in that transaction. A sold Listing remains as history while the active item record lives in the buyer Backpack.

Production /play/ loads play/main.js, which imports play/play-market.js. The production market derives source type and index from each validated chain slot: ordinary records use Backpack indexes, while custodied toolbar records use PlayerEquipment slot indexes. Local-only, syncing, equipped-without-custody, and blueprint records are excluded before submission.

The production cancel UI supplies Listing identity only. Its chain helper loads the explicit destination when one is supplied by another caller or resolves the owner's currently equipped Backpack, then rejects missing or full destinations before building the eight-account cancellation instruction. Purchase independently verifies that the selected destination Backpack belongs to the buyer and has capacity.

After a successful listing, cancellation, or purchase, production reloads chain listings and inventory state rather than treating a browser-local row as custody evidence. The contract's complete source record and state byte remain authoritative.

  1. Create snapshots then removes

    Market copies the full validated 80-byte source record. Backpack source densely removes its record; equipment source clears the selected PlayerEquipment custody slot and published Profile identity.

  2. Cancel appends to a seller destination

    A valid seller Backpack receives the saved record; the Listing remains allocated with state canceled, zero buyer, and settlement slot and time.

  3. Buy pays and appends atomically

    Payment and buyer-Backpack append share one transaction, so any failure rolls the whole transaction back.

  4. Production preserves source identity

    Production play/play-market.js derives source type and index from validated chain custody, while the chain helper resolves and validates a cancellation destination Backpack before submission.

Generated voxel village market where a forged tool already rests inside a central open wooden listing cabinet, the canonical girl stands beside an empty seller Backpack and tray, and the canonical boy stands beside an open destination Backpack.
The complete tool inside the central wooden cabinet, the empty seller side, and the waiting destination Backpack illustrate Listing custody and later cancel-or-buy restoration. The generated cabinet, trays, scale, villagers, and route are metaphors only; source matching, payment order, record bytes, rollback, and Listing state must be established from code and transaction evidence.

RETAINED PRE-ARCHITECTURE DEVNET OBSERVATION

The 18 July snapshot documents history, not the current equipment contract

At confirmed context slot 477,030,223 on 18 July 2026, the retained account observation found ten current version-3 Backpack accounts under the unified Game Program, belonging to eight distinct stored owners. Nine were nonempty. Together they held 205 dense records: 200 blocks, three forged items, one material, and one blueprint. Nine Backpacks declared capacity 50 and one historical account declared 51; each held 56,904,960 lamports. The capacity-51 account was created at slot 476,437,186, before the retained current Game deployment slot 476,942,846, so it cannot prove that this deployment still creates capacity 51.

The same context found no version-5 Listing under the unified Game Program. Of fifteen 773-byte Profile candidates under Player, eleven were owner-derived version-7 Profiles. Their 99 equipment fields were all the zero public key. Five of those eleven Profiles selected a Backpack; all five addresses existed in the retained Backpack set and stored the same owner. The audit did not query the later 7,040-byte PlayerEquipment account family, so these zero fields cannot be used as a current-custody inventory count.

Each of the three observed forged Backpack records used item_pda to name an owner-matched Profile candidate: two canonical version-7 Profiles and one older noncanonical version-3 Profile. That is positive evidence of the former shared-Profile-key forge design and direct evidence that this snapshot predates the current unique ForgedItem architecture. Current source instead requires item_pda to be PDA(forged-item-v1, creator, item ID), creates a 752-byte immutable model account, and will not manufacture the Profile-key pattern described by this historical capture.

The retained audit did not capture ForgedItem accounts, and those three historical records do not point to ForgedItem addresses. A current client would therefore refuse to treat the old item_pda values as validated canonical NCF1 accounts. This is expected testnet architecture drift, not permission to add a legacy fallback: project rules require the final format only, and testnet state may be reset.

The standalone Backpack Program had no 8,048-byte Backpack account in the observation. A separate standalone Market Program held seven older version-3 Listing accounts—six active and one sold—while the reviewed current Listing layout is version 5 and production Play scans the unified Game domain. Those older accounts are useful deployment-drift evidence, not proof that the current 216-byte flow has been exercised.

A separate confirmed 16 July Player audit found all 120 rich-equipment records across ten Appearance accounts empty. It is not part of this slot's account context. The safe statement is narrow: the retained observations prove historical Backpack inventory, three old-format forged records, empty Profile reference fields, and no current unified Listing at that moment. They do not show a current PlayerEquipment transfer, current ForgedItem creation, tag 15 durability settlement, or current unified Market custody transition.

These are account-state facts at one confirmed slot. The audit did not claim a reproducible source-to-deployed-binary match for Player, Backpack, Game, or Market in this page. Later transactions can change every count, balance, pointer, and Listing state.

Unified Game Backpacks 10 accounts · 205 records

200 block + 3 forged + 1 material + 1 blueprint at slot 477,030,223.

Player Profiles 11 canonical v7 · 99 zero equipment fields

Historical Profile fields only; the audit did not query the later PlayerEquipment account family.

Old-format forged outputs 3 records · 3 owner-matched Profile keys

Their shared Profile-key pattern contradicts the current unique ForgedItem format and dates the snapshot.

Current unified Listings 0 accounts of 216 bytes

Seven separate old version-3 standalone-Market accounts are legacy drift, not current-flow evidence.

Generated voxel chain observation deck where the canonical villagers inspect a group of Backpacks, a few forged tools, an empty equipment rack, and an empty market display through survey instruments.
The observation deck, Backpacks, tools, empty rack, and empty display make a point-in-time audit intuitive without printing false precision into the artwork. Only two miniature tools are clearly visible, and every visible count, distance, instrument, and status is metaphorical; only the retained slot-477,030,223 account bytes and decoder establish the dated figures.

READ THE IMPLEMENTATION

Nine equations and twenty-one literal excerpts separate references, custody, rendering, mining, and production reachability

The equations describe Profile and PlayerEquipment identity, immutable ForgedItem identity, complete record layout, validated NCF1 reconstruction, chain durability settlement, source-specific Market custody transitions, and the dated historical inventory. Every excerpt is a continuous substring from the reviewed current repository and follows production /play/.

PlayerEquipment stores nine complete custody slots

PlayerEquipmentBytes = 128 + 9 × 768 = 7,040; ProfileEquipmentBytes = 9 × 32 = 288

Each PlayerEquipment slot can hold source Backpack data, one complete 80-byte record, and up to 640 model bytes. The corresponding Profile field publishes only a derived identity.

128
PlayerEquipment header bytes containing layout, owner, Profile, GlobalConfig, count, and timestamps.
768
One equipment slot: state and source metadata, 80-byte record, model region, and reserved bytes.

Equip moves or exchanges one complete record

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

The Player-to-Game CPI and PlayerEquipment write are atomic. The first branch decreases Backpack item count; the second preserves count by replacing the selected Backpack position with the old equipment record.

i
The selected dense Backpack record index.
E[s]
The complete custodied record and model state in PlayerEquipment slot s.

Unequip returns custody before clearing

backpackIndex = 255 ∧ E_before[s] = r ⇒ B_after = append(B_before, r) ∧ E_after[s] = empty ∧ Profile_after[s] = 0^32

The supplied owner Backpack must have capacity and pass MaterialPhysics mass validation. Any failure rolls back the append and both clears.

append
Add the complete record at the current dense Backpack tail.
0^32
The all-zero identity published for an empty equipment slot.

Profile publishes a hash of current custodied bytes

Profile[s] = hashv("equipment-v2", E[s].sourceBackpack, E[s].record80)

The identity excludes model bytes and slot number but includes mutable durability inside record80. Current tag 15 changes durability without republishing Profile, so a worn slot no longer satisfies this equality until another identity-writing operation runs.

hashv
Solana's 32-byte hash over the concatenated tagged byte slices.
record80
The exact complete record currently stored in the equipment slot.

A current forge creates one unique persistent model account

F = PDA("forged-item-v1", creator, u64LE(itemId)); BackpackRecord.item_pda = F; |F| = 752 bytes

The account must be uninitialized, stores the complete verified NCF1 payload, and has no current mutation instruction after creation. Reusing creator plus item ID is rejected.

u64LE(itemId)
The forged output's eight-byte little-endian item identifier.
F
The immutable ForgedItem PDA and the record's item_pda value.

Canonical NCF1 is recoverable from chain custody

Backpack or Listing: Model = ForgedItem.code; PlayerEquipment: Model = E[s].model; 14 ≤ |Model| ≤ 640 ∧ version = 15 ∧ FNV1a32(Model) = designHash32

Browser storage may cache presentation state, but it is not required as the canonical forged model archive. Every accepted path still checks NCF1 bounds, final version, and design hash.

designHash32
The unsigned 32-bit record metadata used to verify NCF1 bytes.
E[s].model
The verified NCF1 copy held with the custodied equipment record.

Mining and forged durability settle together

Success ⇒ MineCommitted ∧ E_after[s].durability = E_before[s].durability − d; Failure ⇒ Chain_after = Chain_before ∧ LocalWearRestored

Production converts only custodied forged hotbar damage into Player tag 15 instructions. The break-block session must be active, d must be positive, and d cannot exceed current durability.

d
Normalized accumulated damage for one PlayerEquipment slot.

Listing moves the full record through one authoritative source

Create: L.record = source.record ∧ remove(source); Cancel: SellerBackpack′ = append(L.record) ∧ L.state = canceled; Buy: Payment ∧ BuyerBackpack′ = append(L.record) ∧ L.state = sold

Equipment source additionally requires Profile identity to match the current PlayerEquipment record. Canceled and sold Listing accounts remain allocated as history; they are not closed or drained.

L.record
The complete 80-byte source record copied into the 216-byte Listing.

The retained historical Backpack total is exactly decomposed

205 = 200 block + 3 forged + 1 material + 1 blueprint

This arithmetic belongs only to the pre-current-architecture confirmed slot-477,030,223 observation. Its three forged records use the old Profile-key pattern and are not examples of current ForgedItem output.

205
The sum of decoded active records across ten unified-Game Backpacks at the snapshot slot.

PlayerEquipment derives the identity published by PlayerProfile

Rust programs/nicechunk_player/src/state.rs
    pub fn slot_identity(data: &[u8], slot: u8) -> Result<Pubkey, NicechunkPlayerError> {
        let offset =
            Self::slot_offset(slot).map_err(|_| NicechunkPlayerError::InvalidEquipmentSlot)?;
        Self::validate_layout(data)?;
        if data[offset + Self::RECORD_STATE_OFFSET] != 1 {
            return Ok(Pubkey::default());
        }
        Ok(Pubkey::new_from_array(
            solana_program::hash::hashv(&[
                b"equipment-v2",
                &data[offset + Self::RECORD_BACKPACK_OFFSET
                    ..offset + Self::RECORD_BACKPACK_OFFSET + 32],
                &data[offset + Self::RECORD_BACKPACK_SLOT_OFFSET
                    ..offset + Self::RECORD_BACKPACK_SLOT_OFFSET + BACKPACK_SLOT_RECORD_LEN],
            ])
            .to_bytes(),
        ))
    }

An empty slot publishes zero. An active slot hashes the source Backpack address and all 80 current record bytes under the equipment-v2 tag; item_pda, slot index, and model bytes are not the Profile value by themselves.

  1. Lines 1–7

    Validate the slot and complete account layout, returning the zero identity for an empty slot.

  2. Lines 8–19

    Hash the tag, source Backpack address, and complete record into the 32-byte identity.

The transfer processor moves custody between Backpack and PlayerEquipment

Rust programs/nicechunk_player/src/lib.rs
    let clears_slot = backpack_index == CLEAR_EQUIPMENT_BACKPACK_INDEX;
    let (was_equipped, was_custodied) = {
        let equipment_data = player_equipment.try_borrow_data()?;
        PlayerEquipment::validate_owner_and_config(
            &equipment_data,
            authority.key,
            player_profile.key,
            global_config.key,
        )?;
        (
            PlayerEquipment::slot_is_equipped(&equipment_data, slot)?,
            PlayerEquipment::slot_is_custodied(&equipment_data, slot)?,
        )
    };

    let mut next_identity = Pubkey::default();
    if clears_slot {
        if was_custodied {
            invoke_backpack_equipment_transfer(
                game_program,
                transfer_authority,
                authority,
                backpack,
                player_equipment,
                material_physics,
                &[1, 11, slot],
                transfer_bump,
            )?;
        }
        if was_equipped {
            let mut equipment_data = player_equipment.try_borrow_mut_data()?;
            PlayerEquipment::clear_slot(&mut equipment_data, slot, clock.slot)?;
        }
    } 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)

Clearing returns a custodied record to Backpack. Equipping validates the Backpack record, transfers it into PlayerEquipment, stores the full record and model bytes, and publishes only the resulting identity through PlayerProfile.

  1. Lines 1–13

    Validate PlayerEquipment and remember whether the selected slot currently carries an equipped identity and an item held in equipment custody.

  2. Lines 15–31

    A clear request returns a custodied item through the Backpack transfer instruction, then clears any remaining equipped slot state.

  3. Lines 32–66

    An equip request validates one Backpack record, moves custody, stores the record and model in PlayerEquipment, and derives the public slot identity.

  4. Lines 68–69

    PlayerProfile receives only that resulting identity, while the complete item record remains in PlayerEquipment custody.

Current transfer accepts complete block or item records

Rust programs/nicechunk_player/src/state.rs
    pub fn equipment_record_at(
        data: &[u8],
        owner: &Pubkey,
        index: u8,
    ) -> Result<[u8; BACKPACK_SLOT_RECORD_LEN], NicechunkPlayerError> {
        Self::validate_owner(data, owner).map_err(|_| NicechunkPlayerError::InvalidBackpackData)?;
        if index >= data[BACKPACK_ITEM_COUNT_OFFSET] {
            return Err(NicechunkPlayerError::InvalidBackpackItem);
        }
        let offset = BACKPACK_HEADER_LEN + index as usize * BACKPACK_SLOT_RECORD_LEN;
        if offset + BACKPACK_SLOT_RECORD_LEN > data.len() {
            return Err(NicechunkPlayerError::InvalidBackpackData);
        }
        let kind = data[offset];
        if (kind != BACKPACK_SLOT_KIND_BLOCK && kind != BACKPACK_SLOT_KIND_ITEM)
            || read_u32(data, offset + BACKPACK_SLOT_QUANTITY_OFFSET) == 0
        {
            return Err(NicechunkPlayerError::InvalidBackpackItem);
        }
        if kind == BACKPACK_SLOT_KIND_ITEM
            && (read_u64(data, offset + BACKPACK_SLOT_ITEM_ID_OFFSET) == 0
                || data[offset + BACKPACK_SLOT_ITEM_PDA_OFFSET
                    ..offset + BACKPACK_SLOT_ITEM_PDA_OFFSET + 32]
                    .iter()
                    .all(|byte| *byte == 0))
        {
            return Err(NicechunkPlayerError::InvalidBackpackItem);
        }
        data[offset..offset + BACKPACK_SLOT_RECORD_LEN]
            .try_into()
            .map_err(|_| NicechunkPlayerError::InvalidBackpackData)
    }

The current path returns the complete 80-byte record and accepts both block and item kinds. It requires quantity for either kind and nonzero item ID plus item_pda for item records; forged-specific NCF1 validation happens separately when PlayerEquipment writes the slot.

  1. Lines 1–13

    Validate the owner-bound Backpack layout, dense index, and complete record bounds.

  2. Lines 14–19

    Accept only block or item records with nonzero quantity.

  3. Lines 20–33

    Require identifiers for item records, then return all 80 bytes rather than one pointer.

Forge appends with the immutable ForgedItem PDA, then invokes forging XP

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,
        )?
    };

    add_forging_xp_to_player(
        program_id,
        owner,
        backpack,
        player_profile,
        player_program,
        system_program_account,
        &outcome,
    )
}

The Backpack record receives forged_item.key, not PlayerProfile. The immutable ForgedItem account identifies this output, and the following Player CPI adds XP in the same atomic transaction.

  1. Lines 1–13

    Borrow Backpack mutably and settle verified materials into one forged output whose stored item_pda is the supplied immutable ForgedItem account.

  2. Lines 15–23

    Pass the completed forge outcome to the Player Program CPI so forging XP is recorded before the surrounding transaction can succeed.

Forge derives, creates, and packs one immutable ForgedItem

Rust programs/nicechunk_backpack/src/lib.rs
    let item_id_bytes = item_id.to_le_bytes();
    let (expected_forged_item, forged_item_bump) = Pubkey::find_program_address(
        &[FORGED_ITEM_SEED, owner.key.as_ref(), &item_id_bytes],
        program_id,
    );
    require_key_eq(
        forged_item.key,
        &expected_forged_item,
        NicechunkBackpackError::InvalidForgedItemPda,
    )?;
    if forged_item.owner == program_id {
        return Err(NicechunkBackpackError::ForgedItemAlreadyInitialized.into());
    }
    if forged_item.owner != &system_program::ID || forged_item.data_len() != 0 {
        return Err(NicechunkBackpackError::InvalidSystemAccount.into());
    }
    create_forged_item_pda(
        owner,
        forged_item,
        system_program_account,
        program_id,
        item_id,
        forged_item_bump,
    )?;
    {
        let mut forged_item_data = forged_item.try_borrow_mut_data()?;
        ForgedItemAccount::pack(
            &mut forged_item_data,
            &ForgedItemInitArgs {
                bump: forged_item_bump,
                item_id,
                creator: owner.key,
                origin_backpack: backpack.key,
                design_hash,
                code,
                created_slot: clock.slot,
                created_at: clock.unix_timestamp,
            },
        )?;
    }

The address is unique to creator and item ID, must still be an empty System account, and is allocated and packed with the complete verified model before the Backpack output is appended.

  1. Lines 1–10

    Derive and require the exact forged-item-v1 PDA for creator and little-endian item ID.

  2. Lines 11–24

    Reject reuse or a nonempty foreign account, then create the final-sized PDA.

  3. Lines 25–41

    Pack creator, origin Backpack, design hash, complete NCF1 bytes, and creation observations.

Backpack stores the unique ForgedItem address with outcome fields

Rust programs/nicechunk_backpack/src/state.rs
        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)?;
        Ok(outcome)

item_pda receives the already-derived ForgedItem address. The same record carries item ID, calculated mass and volume, durability, grade, level, quality, and design hash before it enters the dense Backpack.

  1. Lines 1–9

    Construct one forged item record and copy its unique ForgedItem PDA.

  2. Lines 10–16

    Store calculated volume, durability, grade, level, quality, and design hash.

  3. Lines 17–18

    Append the complete record to the dense Backpack.

NCF1 equipment bytes must pass length, version, and hash checks

JavaScript play/forge-equipment-payload.js
export const NCF1_EQUIPMENT_VERSION = 15;
export const NCF1_EQUIPMENT_MIN_RAW_BYTES = 14;
export const NCF1_EQUIPMENT_MAX_RAW_BYTES = 640;

export function equipmentPayloadBytes(value) {
  if (value instanceof Uint8Array) return value.length ? value : null;
  if (Array.isArray(value)) return value.length ? Uint8Array.from(value) : null;
  if (value instanceof ArrayBuffer) return value.byteLength ? new Uint8Array(value) : null;
  if (ArrayBuffer.isView(value)) {
    return value.byteLength
      ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
      : null;
  }
  return null;
}

export function validatedNcf1EquipmentPayload(value, expectedDesignHash) {
  const bytes = equipmentPayloadBytes(value);
  const designHash = Math.trunc(Number(expectedDesignHash) || 0) >>> 0;
  if (!bytes
    || !designHash
    || bytes.length < NCF1_EQUIPMENT_MIN_RAW_BYTES
    || bytes.length > NCF1_EQUIPMENT_MAX_RAW_BYTES
    || !isSupportedNcf1EquipmentVersion(bytes[0] >> 4)
    || forgePayloadHash(bytes) !== designHash) return null;
  return bytes;
}

function isSupportedNcf1EquipmentVersion(version) {
  return version === NCF1_EQUIPMENT_VERSION;
}

The validator requires a nonzero design hash, 14-640 bytes, the current version-15 nibble, and exact FNV-1a equality. Version 14 is intentionally rejected because this testnet build has no legacy fallback.

  1. Lines 1–15

    Declare the final NCF1 envelope and normalize supported byte-container types without inventing replacement payload bytes.

  2. Lines 17–26

    Reject missing hashes, out-of-range lengths, non-v15 bytes, and any payload whose computed design hash differs.

  3. Lines 28–30

    Accept only the current version nibble, with no historical-version fallback.

Production Guardian validates forged presentation on send and receive

JavaScript play/play-guardian.js
export function guardianEquipmentFromAvatarEquipment(equipment = {}) {
  const rightHand = String(equipment?.rightHand || "empty");
  if (rightHand === "pickaxe") {
    const designHash = Math.trunc(Number(equipment.designHash) || 0) >>> 0;
    const forged = Boolean(equipment.forged || equipment.isForged || designHash);
    return {
      rightHandKind: forged ? EQUIPMENT_KIND.forged : EQUIPMENT_KIND.pickaxe,
      rightHandVariant: forged ? 2 : 1,
      flags: forged ? EQUIPMENT_FLAG_FORGED : 0,
      designHash: forged ? designHash : 0,
      payloadBytes: forged ? validatedNcf1EquipmentPayload(equipment.payloadBytes, designHash) : null,
    };
  }
  if (rightHand === "block") {
    const blockId = Math.trunc(equipment.blockId || 0);
    return { rightHandKind: EQUIPMENT_KIND.block, rightHandVariant: BLOCK_VARIANT_BY_ID.get(blockId) || clampByte(blockId), flags: 0, designHash: blockId >>> 0 };
  }
  return { rightHandKind: EQUIPMENT_KIND.empty, rightHandVariant: 0, flags: 0, designHash: 0 };
}

export function equipmentFromGuardianEvent(event = {}) {
  const kind = Math.trunc(event?.rightHandKind ?? EQUIPMENT_KIND.empty);
  if (kind === EQUIPMENT_KIND.pickaxe || kind === EQUIPMENT_KIND.forged) {
    const designHash = Math.trunc(Number(event.designHash) || 0) >>> 0;
    const forged = Boolean(
      kind === EQUIPMENT_KIND.forged
      || (Math.trunc(Number(event.flags) || 0) & EQUIPMENT_FLAG_FORGED)
      || designHash
      || Math.trunc(Number(event.rightHandVariant) || 0) === 2
    );
    return forged
      ? { rightHand: "pickaxe", forged: true, designHash, payloadBytes: validatedNcf1EquipmentPayload(event.payloadBytes, designHash) }
      : { rightHand: "pickaxe" };
  }
  if (kind === EQUIPMENT_KIND.block) return { rightHand: "block", blockId: Math.trunc(event.rightHandVariant || 0), color: [0.52, 0.72, 0.38, 1] };
  return { rightHand: "empty" };
}

Production encodes only right-hand kind and variant, a forged flag, design hash, and optional validated NCF1. Incoming forged bytes pass through the same validator. It embeds neither Profile equipment keys nor Backpack presentation fields and proves no custody.

  1. Lines 1–18

    Encode current right-hand presentation and validate forged bytes before sending.

  2. Lines 20–33

    Decode forged presentation while applying the same payload validation.

  3. Lines 34–36

    Reduce block and empty messages to bounded presentation state.

Market snapshots and escrows the selected source atomically

Rust programs/nicechunk_market/src/lib.rs
    let mut backpack_source = None;
    let mut equipment_source = None;
    let source_slot = match args.source_type {
        SOURCE_BACKPACK => {
            let backpack = next_account_info(account_info_iter)?;
            let backpack_program = next_account_info(account_info_iter)?;
            require_key_eq(
                backpack_program.key,
                &NICECHUNK_BACKPACK_PROGRAM_ID,
                NicechunkMarketError::InvalidBackpackProgram,
            )?;
            let source_slot =
                read_backpack_slot_for_listing(backpack, seller.key, args.source_index)?;
            backpack_source = Some((backpack, backpack_program));
            source_slot
        }
        SOURCE_EQUIPMENT => {
            let player_profile = next_account_info(account_info_iter)?;
            let player_equipment = next_account_info(account_info_iter)?;
            let global_config = next_account_info(account_info_iter)?;
            let player_program = next_account_info(account_info_iter)?;
            require_key_eq(
                player_program.key,
                &NICECHUNK_PLAYER_PROGRAM_ID,
                NicechunkMarketError::InvalidPlayerProgram,
            )?;
            let source_slot = read_equipment_slot_for_listing(
                player_profile,
                player_equipment,
                seller.key,
                args.source_index,
                player_program.key,
                global_config.key,
            )?;
            equipment_source = Some((
                player_profile,
                player_equipment,
                global_config,
                player_program,
            ));
            source_slot
        }
        _ => return Err(NicechunkMarketError::InvalidInstruction.into()),
    };
    let market_user = next_account_info(account_info_iter)?;
    validate_transferable_source_slot(&source_slot)?;

    let clock = Clock::get()?;
    validate_existing_market_user(program_id, market_user, seller.key)?;
    if !market_user.is_writable {
        return Err(NicechunkMarketError::InvalidMarketUser.into());
    }
    {
        let mut data = market_user.try_borrow_mut_data()?;
        MarketUserState::validate(&data, seller.key)?;
        MarketUserState::increment_active(&mut data, clock.slot)?;
    }

    create_listing_pda(
        seller,
        listing,
        system_program_account,
        program_id,
        args.listing_id,
        bump,
    )?;

    {
        let mut data = listing.try_borrow_mut_data()?;
        ListingAccount::pack(
            &mut data,
            &ListingInitArgs {
                bump,
                seller: seller.key,
                listing_id: args.listing_id,
                currency: args.currency,
                source_type: args.source_type,
                source_index: args.source_index,
                price_base_units: args.price_base_units,
                source_slot,
                created_slot: clock.slot,
                created_at: clock.unix_timestamp,
            },
        )?;
    }

    if let Some((backpack, backpack_program)) = backpack_source {
        remove_backpack_resource(seller, backpack, backpack_program, args.source_index as u16)?;
    }
    if let Some((player_profile, player_equipment, global_config, player_program)) =
        equipment_source
    {
        release_player_equipment_to_market(
            seller,
            player_profile,
            player_equipment,
            global_config,
            listing,
            player_program,
        )?;
    }
    Ok(())

Backpack source copies one dense record; equipment source reads one custodied PlayerEquipment slot. Membership, Listing creation, and source removal all succeed or roll back together.

  1. Lines 1–16

    For a Backpack source, validate the Backpack Program and copy the selected dense slot before mutation.

  2. Lines 17–43

    For an equipment source, validate Player Program custody and snapshot the selected PlayerEquipment slot.

  3. Lines 44–81

    Validate transferability and seller membership, increment active count, create the Listing PDA, and pack the copied source record.

  4. Lines 83–99

    Remove exactly the chosen Backpack or equipment source; any later error rolls the entire Solana instruction back.

Production play-market preserves Backpack or equipment source

JavaScript play/play-market.js
      const result = await module.createMarketListingOnChain({
        item: {
          source: marketSourceForSlot(item.slot),
          backpack: item.slot.chainBackpack,
          slotIndex: marketSourceIndexForSlot(item.slot),
          category,
        },
        currency,
        price,
        backpackAddress: item.slot.chainBackpack,
      });

The caller derives source type and index from the selected slot. Backpack records remain Backpack sources; custodied toolbar records use PlayerEquipment slot indexes.

  1. Lines 1–5

    Build the item descriptor from the selected UI slot, preserving its chain source family and source index.

  2. Lines 6–9

    Pass currency, price, and Backpack context to the chain module without rewriting equipment custody as Backpack custody.

Cancellation lets the chain helper resolve the destination Backpack

JavaScript play/play-market.js
      const result = await module.cancelMarketListingOnChain({
        listing: listing.listing,
        listingId: listing.listingId,
      });
      if (!result?.submitted) {
        handleMarketAccessReason(result?.reason);
        const message = ui("main.market.cancelFailed", "Cancel transaction failed: {reason}", {
          reason: marketSubmissionReason(result?.reason, ui, "cancel"),
        });
        showMarketStatus(message, "warn");
        showTradeToast(message, "error");
        return result;
      }

The UI supplies Listing identity. The chain helper resolves the equipped Backpack when needed, validates membership, and builds the final eight-account cancellation instruction.

  1. Lines 1–4

    Submit only the Listing address and ID from the UI, leaving destination custody resolution to the chain helper.

  2. Lines 5–13

    Convert a rejected submission into localized status and toast feedback, then stop without claiming cancellation succeeded.

Production Play submits equipment changes and reloads chain custody

JavaScript play/play-chain-player.js
    try {
      const module = await loadPlayChainModule();
      if (typeof module.setPlayerEquipmentSlotsOnChain !== "function") {
        throw new Error("equipment-save-unavailable");
      }
      const result = await module.setPlayerEquipmentSlotsOnChain(batch.map((change) => ({
        slot: change.index,
        beforeReference: change.beforeReference || null,
        reference: change.reference || null,
      })));
      if (!result?.submitted) throw new Error(String(result?.reason || "equipment-not-submitted"));
      mutation.lastSubmittedAt = Date.now();
      mutation.lastSignature = String(result.signature || "");
      mutation.lastReason = "saved";
      mutation.lastError = "";
      state.equipment = result.equipment?.initialized ? result.equipment : state.equipment;
      state.equipmentReadKnown = true;
      await refreshBackpack({ force: true, quiet: true });
      if (!pendingEquipmentChanges.size) applyEquipmentSnapshot({ force: true });
      appendEvent(ui("main.equipment.chainSaved", "Equipment saved on chain ({signature}).", {
        signature: shortSignature(mutation.lastSignature),
      }));
    } catch (error) {
      mutation.lastError = readableError(error);
      mutation.lastReason = "error";
      const rollback = batch.filter((change) => !pendingEquipmentChanges.has(change.index));
      getGameState()?.restoreEquipmentMutation?.(rollback);

The production player controller calls the chain equipment writer, adopts the returned PlayerEquipment account, refreshes Backpack state, and reapplies the authoritative snapshot. A failed batch restores local mutations instead of presenting them as saved.

  1. Lines 1–10

    Load the production chain module and submit exact before/after references for each changed hotbar slot.

  2. Lines 11–22

    Require submission success, adopt returned equipment, refresh Backpack custody, and render the authoritative snapshot.

  3. Lines 23–27

    On failure, retain the error and restore local slot mutations that were not superseded.

Current clients validate and hydrate ForgedItem model accounts

JavaScript src/chain/nicechunkChain.js
async function hydrateForgedItemSlots(slots, conn, reportScope) {
  const candidates = (slots ?? []).filter((slot) => (
    slot?.kindCode === backpackSlotKindItem
    && slot.category === backpackItemCategoryForged
    && slot.itemCode === backpackForgedItemCode
    && slot.itemPda
    && slot.itemPda !== PublicKey.default.toBase58()
  ));
  if (!candidates.length) return slots;

  const addresses = Array.from(new Set(candidates.map((slot) => slot.itemPda))).map(
    (address) => new PublicKey(address),
  );
  let accounts;
  try {
    accounts = await getMultipleAccountsInfoBatched(conn, addresses, 100);
  } catch (error) {
    reportRpcError(error, reportScope);
    return slots;
  }
  const decodedByAddress = new Map();
  for (let index = 0; index < addresses.length; index += 1) {
    const account = accounts[index];
    if (!account?.data?.length || !account.owner.equals(gameContext.backpackProgramId)) continue;
    try {
      const decoded = decodeForgedItem(account.data);
      const [expected] = deriveForgedItemPda(
        new PublicKey(decoded.creator),
        decoded.itemId,
        gameContext.backpackProgramId,
      );
      if (!expected.equals(addresses[index])) continue;
      decodedByAddress.set(addresses[index].toBase58(), decoded);
    } catch {
      // Invalid immutable model accounts are ignored and cannot be equipped.
    }
  }

  for (const slot of candidates) {
    const forgedItem = decodedByAddress.get(slot.itemPda);
    if (
      !forgedItem
      || forgedItem.itemId !== slot.itemId
      || forgedItem.designHash !== (Number(slot.metadata) >>> 0)
    ) {
      continue;
    }
    slot.forgedItem = forgedItem;
    slot.modelBytes = Array.from(forgedItem.codeBytes);
  }
  return slots;
}

Backpack and Listing hydration loads unique item_pda accounts in batches, validates program ownership and PDA derivation, and attaches model bytes only when item ID and design hash also match the containing record.

  1. Lines 1–19

    Select current forged records, deduplicate their item_pda addresses, and batch-fetch account data.

  2. Lines 20–34

    Require the configured program owner, decode the final layout, and rederive each PDA from creator and item ID.

  3. Lines 36–49

    Match item ID and design hash before exposing canonical NCF1 bytes to rendering or equip.

Reach radius comes from the selected tool mesh

JavaScript chunk.js/physics/voxel-item-collision.js
  function toolReachSphere({ yaw = null, swing = null } = {}) {
    const geometry = selectedToolGeometry();
    if (!geometry?.avatarMesh || !geometry.collisionParts.length) return fallbackToolReachSphere(toolYaw(yaw, swing));
    const pivot = geometry.avatarMesh.pivots?.right_hand_item ?? geometry.avatarMesh.pivots?.right_arm;
    if (!Array.isArray(pivot)) return fallbackToolReachSphere(toolYaw(yaw, swing));
    if (reachCacheMesh !== geometry.avatarMesh || reachCacheEquipmentId !== geometry.equipmentId) {
      reachCacheMesh = geometry.avatarMesh;
      reachCacheEquipmentId = geometry.equipmentId;
      reachCacheRadius = toolReachRadius(geometry.collisionParts, pivot);
    }
    const center = avatarLocalToWorld(pivot, toolYaw(yaw, swing));
    return {
      x: center[0],
      y: center[1],
      z: center[2],
      centerX: center[0],
      centerY: center[1],
      centerZ: center[2],
      radius: reachCacheRadius,
      radiusSquared: reachCacheRadius * reachCacheRadius,
      equipmentId: geometry.equipmentId,
    };
  }

The resolver takes collision parts from the selected avatar tool and its right-hand pivot, calculates a radius, and places that sphere in world space. Forged geometry is attached only after local runtime and grip restoration, so this is presentation logic rather than a Profile-slot check.

  1. Lines 1–5

    Resolve selected geometry and its right-hand pivot, with a basic fallback only when geometry is unavailable.

  2. Lines 6–10

    Recalculate reach when mesh or equipment identity changes.

  3. Lines 11–22

    Transform the hand pivot into world space and return the geometry-derived sphere.

Production resamples collision frames throughout the swing

JavaScript play/mining-controller.js
  function toolCollisionHitForProgress(swing, toProgress) {
    const endProgress = clamp01(toProgress);
    const startProgress = Math.max(collisionStartProgress, Math.min(endProgress, Number(swing.previousToolProgress) || collisionStartProgress));
    let previousBoxes = swing.previousToolBoxes;
    let previousCount = swing.previousToolBoxCount || 0;
    if (!previousBoxes || !previousCount) {
      const startFrame = getToolCollisionFrame?.({ progress: startProgress, swing });
      const startBoxes = Array.isArray(startFrame?.boxes) ? startFrame.boxes : [];
      const startHit = toolCollisionHit(swing.hit, startBoxes, null, 0);
      if (startHit.hit) return startHit;
      previousBoxes = copyToolBoxes(startBoxes, previousBoxes);
      previousCount = startBoxes.length;
    }

    const stepCount = Math.max(1, Math.ceil(Math.max(0.001, endProgress - startProgress) / 0.10));
    for (let step = 1; step <= stepCount; step += 1) {
      const progress = startProgress + (endProgress - startProgress) * (step / stepCount);
      const frame = getToolCollisionFrame?.({ progress, swing });
      const boxes = Array.isArray(frame?.boxes) ? frame.boxes : [];
      if (!boxes.length) {
        previousBoxes = null;
        previousCount = 0;
        continue;
      }
      const hitResult = toolCollisionHit(swing.hit, boxes, previousBoxes, previousCount);
      if (hitResult.hit) return hitResult;
      previousBoxes = copyToolBoxes(boxes, previousBoxes);
      previousCount = boxes.length;
    }
    swing.previousToolBoxes = copyToolBoxes(previousBoxes ?? [], swing.previousToolBoxes);
    swing.previousToolBoxCount = previousCount;
    swing.previousToolProgress = endProgress;
    return { hit: false };
  }

The active swing does not accept its initial reach result as final contact. It subdivides animation progress, samples tool boxes, and carries each previous frame forward for swept testing.

  1. Lines 1–13

    Establish and directly test the first collision frame.

  2. Lines 15–29

    Subdivide elapsed animation progress and test every sampled frame.

  3. Lines 30–34

    Retain the latest boxes and progress for the next update.

Each sampled frame checks direct and swept AABB contact

JavaScript play/mining-controller.js
  function toolCollisionHit(hit, boxes, previousBoxes, previousBoxCount) {
    const targetBox = createBlockAabb(hit.worldX, hit.worldY, hit.worldZ, blockCollisionPadding, scratchTargetBox);
    for (let index = 0; index < boxes.length; index += 1) {
      const box = boxes[index];
      if (aabbIntersectsAabb(box, targetBox)) {
        return collisionResultFromBox(box, targetBox, 1);
      }
      const previous = previousBoxes?.[index];
      if (!previous || index >= previousBoxCount) continue;
      const result = sweptAabbIntersection(previous, box, targetBox, scratchCollisionResult);
      if (result.hit) return { hit: true, pointX: result.x, pointY: result.y, pointZ: result.z, time: result.time };
    }
    return { hit: false };
  }

A current tool box may overlap the block directly. Otherwise the controller sweeps from the previous box to the current box so a fast-moving tool cannot skip through the target between samples.

  1. Lines 1–6

    Build the target block AABB and accept direct overlap.

  2. Lines 7–11

    Sweep the corresponding previous tool box toward the current one.

  3. Lines 12–14

    Reject contact when neither direct nor swept testing hits.

Mining damage spends local hotbar durability

JavaScript play/mining-controller.js
    const key = damageKey(hit);
    const now = performance.now();
    prunePartialDamage(now, key);
    const damageState = partialDamageState(key, now);
    const nextDamage = damageState.damage + 1;
    const requiredDamage = Math.max(1, swing.requiredDamage || requiredDamageForBlock(hit.blockId, def));
    damageState.damage = nextDamage;
    damageState.lastTouchedAt = now;
    damageState.toolDamageBySlot.set(
      swing.selectedToolIndex,
      (damageState.toolDamageBySlot.get(swing.selectedToolIndex) ?? 0) + 1,
    );
    selectedTool.durability = Math.max(0, Math.trunc(selectedTool.durability || 0) - 1);
    gameState.saveHotbarSlots();

After target and physical-contact checks, the controller records and displays tentative wear immediately. When the break becomes pending, the accumulated wear for a custodied forged slot is converted into chainToolDamage for atomic PlayerEquipment settlement.

  1. Lines 1–8

    Update partial damage for the target.

  2. Lines 9–12

    Attribute one local durability point to the selected hotbar slot.

  3. Lines 13–14

    Decrement and persist browser hotbar durability.

Rolling back a pending mine restores recorded local tool damage

JavaScript play/mining-controller.js
  function rollbackPendingAt(index) {
    const pending = index >= 0 ? pendingTx.splice(index, 1)[0] : null;
    if (!pending) {
      onStatus("No pending delta to rollback.");
      return null;
    }
    const damageEntries = Array.isArray(pending.toolDamageBySlot) && pending.toolDamageBySlot.length
      ? pending.toolDamageBySlot
      : Number.isInteger(pending.toolSlotIndex) && Number(pending.toolDamage) > 0
        ? [{ slotIndex: pending.toolSlotIndex, amount: pending.toolDamage }]
        : [];
    for (const entry of damageEntries) {
      gameState.restoreToolDamage(entry.slotIndex, entry.amount);
    }
    gameState.playerProfile.rolledBackMines += 1;
    gameState.playerProfile.minedBlocks = Math.max(0, gameState.playerProfile.minedBlocks - Math.max(1, Math.trunc(pending.minedBlockCount || 1)));
    gameState.savePlayerProfile();
    onRollback(pending);
    onChanged();
    return pending;
  }

A pending mine retains a per-slot damage ledger. Rollback restores every recorded local slot before updating browser statistics. Production calls this when mining submission is disabled, unavailable, rejected, or throws; it is not an on-chain durability refund.

  1. Lines 1–6

    Remove the matching pending action or stop if it was already resolved.

  2. Lines 7–13

    Recover the per-slot damage ledger, including the older single-slot fallback.

  3. Lines 14–21

    Restore local tool damage and update rollback state.

The production adapter forwards explicit chain equipment damage

JavaScript play/play-chain-adapter.js
    const mineOptions = mineShouldSavePlayerPosition(block)
      ? { ...backpackOptions, durabilityDamage: pending.chainToolDamage, playerPosition: playerPositionProof(getPlayerPosition) }
      : { ...backpackOptions, durabilityDamage: pending.chainToolDamage };

The adapter carries the normalized equipment-slot damage ledger into every supported production mine helper through options.durabilityDamage. This is the authoritative durability input; the separate legacy local toolSlot argument is not used for chain selection.

  1. Lines 1–3

    Attach chainToolDamage whether or not this mine also carries a player-position proof.

The chain helper builds one Player tag 15 instruction per damaged slot

JavaScript src/chain/nicechunkChain.js
function addEquipmentDurabilityInstructions(transaction, { authority, owner, damage = [] } = {}) {
  for (const entry of normalizeEquipmentDurabilityDamage(damage)) {
    transaction.add(createConsumePlayerEquipmentDurabilityInstruction({
      authority,
      owner,
      equipmentSlot: entry.equipmentSlot,
      amount: entry.amount,
    }));
  }
  return transaction;
}

function normalizeEquipmentDurabilityDamage(damage = []) {
  const totals = new Map();
  for (const entry of Array.isArray(damage) ? damage : []) {
    const equipmentSlot = Math.trunc(Number(entry?.equipmentSlot));
    const amount = Math.trunc(Number(entry?.amount));
    if (!Number.isInteger(equipmentSlot) || equipmentSlot < 0 || equipmentSlot >= playerEquipmentSlotCount) continue;
    if (!Number.isInteger(amount) || amount < 1) continue;
    totals.set(equipmentSlot, Math.min(0xffffffff, (totals.get(equipmentSlot) ?? 0) + amount));
  }
  return Array.from(totals, ([equipmentSlot, amount]) => ({ equipmentSlot, amount }));
}

function createConsumePlayerEquipmentDurabilityInstruction({ authority, owner, equipmentSlot, amount }) {
  const [playerProfile] = derivePlayerProfilePda(owner);
  const [playerSession] = derivePlayerSessionPda(owner, authority);
  const [playerEquipment] = derivePlayerEquipmentPda(owner);
  const data = Buffer.alloc(6);
  data.writeUInt8(15, 0);
  data.writeUInt8(equipmentSlot, 1);
  data.writeUInt32LE(amount, 2);
  return new TransactionInstruction({
    programId: playerProgramId,
    keys: [
      { pubkey: authority, isSigner: true, isWritable: true },
      { pubkey: owner, isSigner: false, isWritable: false },
      { pubkey: playerProfile, isSigner: false, isWritable: false },
      { pubkey: playerSession, isSigner: false, isWritable: false },
      { pubkey: playerEquipment, isSigner: false, isWritable: true },
      { pubkey: deriveGlobalConfigPda(), isSigner: false, isWritable: false },
    ],
    data,
  });
}

Damage is grouped by PlayerEquipment slot, invalid or zero entries are discarded, and each accepted total becomes a six-account Player instruction whose tag byte is 15 and whose amount is little-endian u32.

  1. Lines 1–10

    Append one durability instruction for each normalized equipment-slot total.

  2. Lines 12–22

    Reject invalid entries, group duplicate slots, and cap the accumulated u32 amount.

  3. Lines 24–44

    Derive Profile, Session, and PlayerEquipment, encode tag 15, and mark only PlayerEquipment writable.

Player tag 15 decrements only a valid custodied forged record

Rust programs/nicechunk_player/src/state.rs
    pub fn consume_forged_durability(
        dst: &mut [u8],
        slot: u8,
        amount: u32,
        updated_slot: u64,
    ) -> ProgramResult {
        if amount == 0 {
            return Err(NicechunkPlayerError::InvalidDurabilityAmount.into());
        }
        let offset = Self::slot_offset(slot)?;
        Self::validate_layout(dst)?;
        let record_offset = offset + Self::RECORD_BACKPACK_SLOT_OFFSET;
        if dst[offset + Self::RECORD_STATE_OFFSET] != 1
            || dst[offset + Self::RECORD_FLAGS_OFFSET] & PLAYER_EQUIPMENT_FLAG_CUSTODY == 0
            || dst[record_offset] != BACKPACK_SLOT_KIND_ITEM
            || dst[record_offset + 1] != BACKPACK_ITEM_CATEGORY_FORGED
            || read_u16(dst, record_offset + 18) != BACKPACK_FORGED_ITEM_CODE
        {
            return Err(NicechunkPlayerError::EquipmentNotCustodied.into());
        }
        let durability_offset = record_offset + BACKPACK_SLOT_DURABILITY_CURRENT_OFFSET;
        let max_offset = record_offset + BACKPACK_SLOT_DURABILITY_MAX_OFFSET;
        let current = read_u32(dst, durability_offset);
        let maximum = read_u32(dst, max_offset);
        if current == 0 || maximum == 0 || current > maximum || amount > current {
            return Err(NicechunkPlayerError::EquipmentBroken.into());
        }
        dst[durability_offset..durability_offset + 4]
            .copy_from_slice(&(current - amount).to_le_bytes());
        dst[Self::UPDATED_SLOT_OFFSET..Self::UPDATED_SLOT_OFFSET + 8]
            .copy_from_slice(&updated_slot.to_le_bytes());
        Ok(())
    }

The on-chain writer requires active custody, forged category, item code 8, valid current and maximum durability, and enough remaining durability. It changes the record and PlayerEquipment updated slot, but it does not receive or rewrite PlayerProfile, which creates the documented post-wear identity mismatch.

  1. Lines 1–11

    Reject zero damage, invalid slot, or an invalid PlayerEquipment layout.

  2. Lines 12–20

    Require active custody and the exact forged item category and code.

  3. Lines 21–33

    Validate bounds, subtract the amount, and update PlayerEquipment without touching Profile.

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/lib.rs

Dispatches final Player tags 13 through 16 and implements atomic equip or unequip transfer, PlayerEquipment slot swap, session-authorized durability consumption, and validated equipment release to Market.

programs/nicechunk_player/src/state.rs

Defines the 7,040-byte PlayerEquipment layout, nine 768-byte custody slots, equipment-v2 identity, final NCF1 validation, forged durability mutation, nine Profile identities, and twelve currently empty Appearance rich-equipment records.

play/play-chain-player.js

Queues production hotbar mutations, submits them through setPlayerEquipmentSlotsOnChain, adopts returned PlayerEquipment state, refreshes Backpack custody, and restores local mutations after failure.

programs/nicechunk_backpack/src/lib.rs

Implements Game-side Backpack-to-equipment move or exchange, equipment-to-Backpack append, unique ForgedItem PDA creation and packing, verified material consumption, forged record append, and atomic Player forging-XP invocation.

programs/nicechunk_backpack/src/state.rs

Defines the dense 80-byte record, Backpack replacement and removal behavior, final 752-byte immutable ForgedItem account, NCF1 validation, forged outcome fields, and PlayerEquipment custody view used by Game transfers.

programs/nicechunk_market/src/lib.rs

Implements source-specific listing resolution, full-record copy, dense Backpack removal, validated PlayerEquipment release, cancellation restore and state transition, and atomic purchase append.

programs/nicechunk_market/src/state.rs

Defines the current 216-byte version-5 Listing layout, including its 80-byte source record, source metadata, buyer, and timestamps.

programs/nicechunk_market/src/cluster_config.rs

Selects the accepted Backpack Program ID by build feature: unified Game when unified-game is enabled and standalone Backpack otherwise.

programs/nicechunk_game/Cargo.toml

Compiles the embedded Market crate with devnet, no-entrypoint, and unified-game features for the unified Game router.

programs/nicechunk_chunk/src/state.rs

Defines Chunk-side current Profile and Session validation; equipment durability is intentionally settled by the separate Player tag 15 instruction appended to the same client transaction.

src/forgedItems.js

Implements optional browser-side forged presentation caching keyed by design hash; this cache is supplementary to current on-chain ForgedItem and PlayerEquipment model persistence.

play/forge-equipment-payload.js

Validates bounded NCF1 equipment payloads and requires the payload hash to match the supplied design hash before Guardian or runtime use.

play/forged-hotbar-compat.js

Normalizes forged hotbar presentation and keeps record-derived durability and identity fields separate from validated model bytes.

play/game-state.js

Reconstructs hotbar positions from custodied PlayerEquipment records and embedded model bytes, tracks source type and equipment slot, and maintains tentative local durability for responsive interaction.

play/play-avatar-session.js

Builds the selected validated NCF1 runtime and binds forged avatar geometry only when the interaction model exposes a usable grip.

play/tests/player-equipment-chain-state.test.mjs

Exercises exact chain-backed equip and unequip references, embedded-record restoration, slot swaps, and remote forged or block equipment reconstruction from public PlayerEquipment state.

src/chain/nicechunkChain.js

Derives and decodes final PlayerEquipment and ForgedItem PDAs, hydrates canonical model bytes, submits tag 13 and 14 equipment changes, appends tag 15 durability instructions to mining, and builds both Market source families.

play/play-market.js

Implements the production /play/ market path that derives Backpack or PlayerEquipment custody, submits chain-only listings, resolves cancellation through the chain helper, and prevents stale refreshes or duplicate operations.

play/index.html

Loads ./main.js as the production /play/ module entry.

play/main.js

Is the production /play/ entry, imports play-market and play-guardian, wires avatar collision frames and targeting into mining, and routes rejected pending mines into rollback.

play/play-guardian.js

Implements the production modular Guardian encoder and decoder with right-hand presentation fields and strict forged NCF1 validation, without Profile keys or Backpack presentation fields.

chunk.js/physics/voxel-item-collision.js

Derives tool reach from selected avatar collision parts and the right-hand pivot, then searches tool trajectories against target AABBs.

play/mining-controller.js

Samples collision boxes through the swing, requires direct or swept AABB contact, records tentative wear, maps custodied forged wear to equipment-slot chainToolDamage, and restores local damage on rollback.

play/play-chain-adapter.js

Passes pending chainToolDamage as options.durabilityDamage to production mine helpers, independently of the legacy local hotbar index argument.

play/play-chain-session.js

Calls mining rollback when submission is disabled, the wallet is unavailable, the adapter rejects the mine, or submission throws.

play/tests/forged-equipment-restore.test.mjs

Verifies strict NCF1 restoration and Guardian payload bounds without inventing geometry for an invalid forged model.

play/tests/mining-controller.test.mjs

Verifies per-slot local durability rollback and authoritative equipment-slot durability damage for confirmed forged-tool mining.

play/tests/market-forged-chain-instruction.test.mjs

Verifies final Backpack and PlayerEquipment decoders, immutable NCF1 v15 ForgedItem decoding, exact Market account layouts, and complete Listing source preservation.

docs/audits/equipment-and-custody-devnet-2026-07-18.json

Retains the single confirmed slot-477,030,223 account context, raw Base64 and hashes, decoded Backpack, Profile, Listing, and legacy-program distributions, forged-to-Profile comparisons, and deployment-boundary notes.

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

Retains the preceding Player account snapshot used to cross-check current Profile and Appearance layouts, including empty rich-equipment records.

docs/visual-provenance/equipment-and-custody.json

Records the image-generation prompts, canonical villager and real Chunk.js scene references, source and output hashes, processing lineage, and one-to-one teaching purpose of all seven visuals.