DETERMINISTIC ADDRESS, OPEN CREATION
A RecipeTable PDA binds its stored ID, fixed recipe authority, and material outputs
RecipeTable version 2 uses the literal smelting-recipes seed and a little-endian 64-bit table ID. Initialization verifies that derivation, requires the fixed recipe-authority wallet 9XuoVVwqP2jipt3jpJVXCSS2N2jr9vDuV3d6K73FKVud as writable signer and payer, records the ID and bump, and stores the same fixed authority. Direct upsert requires that recipe-authority signer. Tag 3 authority transfer has been removed; a finalized Civilization receipt remains the separate governance-mediated patch route.
Execution first requires Smelting-program ownership, then RecipeTable::validate_identity checks the fixed length, magic, version, initialized byte, stored count bound, fixed authority, stored table ID, derived address, and stored bump. It also pins the supplied GlobalConfig to the canonical Core PDA and Core program owner before deriving or validating PlayerProgress.
Every active output is checked again during execution. It must be an Item in the Material category, and its item PDA must equal the executing RecipeTable. A caller cannot substitute an arbitrary program-owned account or use an authority-created table to mint unrelated item categories. The remaining boundary is narrower: execution accepts any correctly formed table created by the fixed recipe authority, rather than consulting the website's configured table-ID list.
The compiled authority is required and cannot be transferred by a Smelting instruction.
The account is re-derived from its stored table ID and checked against the fixed authority.
The stored output points back to the exact validated table used by execution.
TEN RECORDS, FIXED TEMPLATES
Each table reserves ten large recipe records, and disabling one does not free its position
The 96-byte header stores magic, version, bump, initialized state, table ID, authority, recipe count, creation and update slots, and creation time. Each 984-byte record then stores recipe ID, enabled flag, minimum heat tier, active input and output counts, yield basis points, eight full 80-byte input templates, four full 80-byte output templates, and an update slot. A complete table is always 9,936 bytes whether it contains one active recipe or ten.
Input count is constrained to one through eight and output count to one through four. Encoding fills unused fixed positions with copies, but only active counts participate in matching or output. Recipe yield must be one through 10,000. Every active output must pass strict item metadata checks, use the Material category, and set item PDA to the target RecipeTable; these checks run for fixed-authority upsert, Civilization patch, and execution.
Upsert scans all ten records. Reusing a recipe ID replaces its existing position; a new ID uses only a record whose stored recipe ID is zero. Upserting that same ID with enabled false keeps the nonzero ID in its occupied position, but replaces the complete record, including its templates, and writes the current update slot. recipeCount is not reduced. This retained identity is useful for migration history, but a table filled with disabled records is still full and cannot accept another ID without a new table or format change.
A 96-byte header plus ten 984-byte recipe records.
Inactive fixed template positions remain serialized but are ignored by their counts.
find_recipe skips it, while upsert cannot treat its nonzero ID as an empty slot.
UNIQUE INDEXES, EXACT QUANTITIES
Smelting consumes required stack quantities and preserves every remainder
Execution requires the owner wallet itself to sign and be writable. There is no PlayerSession alternative in the Smelting instruction. The supplied Backpack must be owned by the configured Backpack Program, its stored owner must equal the signer, and every selected index must address the current dense record prefix. Input and fuel indexes share one uniqueness bitmap, so one slot cannot serve both roles or appear twice.
Each active RecipeRecord input stores one identity template and a positive quantity. Synchronization writes one template per requirement type, so wooden_stick stores one wooden_plank template with quantity two instead of two duplicate templates. Execution multiplies each template quantity by the batch multiplier, allocates matching units from the selected records, and requires every selected input to contribute. One quantity-four wooden-plank record can therefore supply one wooden-stick batch while retaining quantity two. The Backpack Program retains proportional volume, durability, and MaterialPhysics-derived mass and updates Backpack total mass in the same CPI.
Fuel is a separate hardcoded classifier. Final chain tiers recognize dry grass, dead bush, and thorn at tier one; wood-like blocks at tier two; charcoal material code 1001 at tier three; and coal at tier four. Basalt and lava remain recipe inputs and are not Backpack fuels. Only the maximum submitted fuel tier is compared with minHeatTier. Each submitted fuel index consumes one unit, so a stacked charcoal record remains with proportionally reduced physical state.
The current execute account list has no PlayerProfile or PlayerSession route.
Multiplier scales each template quantity; selected indexes stay unique while one stack may contribute several units.
The highest recognized tier authorizes heat, and each submitted fuel record contributes one consumed unit.
VOLUME IN, BOUNDED RECORDS OUT
Batching scales physical output and stack quantity while failure remains atomic
Before mutation, the program counts only input records that will be fully consumed and fuel records whose quantity is one as released slots, then adds recipe outputCount. Partially consumed material or fuel stacks remain occupied. If the projected count exceeds Backpack capacity, execution stops. The Backpack CPI applies exact quantities, and later append or progress failure rolls the complete Solana transaction back.
Only consumed input shares contribute actual input volume; fuel does not. A partial material stack contributes volume in proportion to consumed quantity. For each active output, the program begins with that output's PDA template volume, scales it by multiplier and by the actual-versus-expected input-volume ratio, then applies recipe yield and the Smelting skill multiplier. Level zero preserves 100 percent of the recipe result, and each level adds 5 percent extra output through level ten, reaching 150 percent.
OutputCount remains the number of appended Backpack records, but output quantity is now authoritative. Primary synchronization stores yieldCount in the output template; execution scales it by batch multiplier and skill. Merge recipes consume every selected same-material stack, preserve their total quantity and physical volume at 100 percent merge yield, and append one consolidated output record.
For item outputs, the program replaces volume, writes the calculated quantity, recomputes durability and minimum item level, and retains table-backed identity. Current synchronization uses the material recipe identity as item ID rather than minting a unique instance ID. Smelting XP is based on consumed input units, excluding fuel. Every mutation belongs to the same atomic instruction.
Retained material and fuel stacks remain occupied before outputCount is added.
Primary output quantity follows yieldCount, batch, and skill; merge quantity equals all consumed units.
Partial consumption, strict append, metadata normalization, mass accounting, and XP update either commit together or do not commit.
BUNDLED RULES VERSUS SUPPLIED TABLE
The current website plans from local rules and does not compare them with the live table
The smelting panel imports src/data/smeltingRules.js through a lightweight adapter. It groups local raw inputs, catalysts, heat tiers, yieldCount, composition, and material identities into recipe cards, derives a table ID and recipe ID from local material order, and asks for at most one fuel record. Quantity-aware planning can satisfy a multi-unit material requirement from one stack and permits up to ninety-eight input records so the combined input-plus-fuel payload stays within ninety-nine.
executeSmeltingOnChain derives the local table ID and checks only that RPC returned nonempty account data before submission. It does not decode RecipeTable or compare inputs, outputs, yield, or heat with the local card. The on-chain program now performs owner, authority, table-identity, GlobalConfig, and output-provenance checks, so malformed or substituted state is rejected; a valid authority-created table can still differ from the local preview and produce a failed match or a different legitimate result.
Merge planning preserves recipeKind. smeltingRecipeChainIdentity selects mergeRecipeId and mergeRecipeTableId for merge candidates, while primary candidates keep their primary identity. Merge selection consumes all units from at least two selected same-material records and consolidates them without the former ninety-percent loss.
Release validation synchronizes all primary and merge records from the canonical rule set, reads every configured RecipeTable back from Devnet, and compares each active record byte-for-byte with the locally encoded expectation. A mismatch in identity, quantity, heat tier, yield, output provenance, or physical template blocks release instead of relying on a stale historical snapshot.
Progress and preview remain program-scoped but are now read together. Execution derives PlayerProgress under the unified-game Smelting Program and canonical Core GlobalConfig. fetchPlayerProgress batches both Chunk and Game/Smelting addresses, taking Precision and Exploration from Chunk and Smelting XP from Game. Locally derived material attributes still use selected slots and a client seed; confirmed output metadata comes from the validated on-chain template and Rust normalization.
One additional fuel index still fits the chain ceiling of ninety-nine.
The program enforces identity and provenance, but the website does not pre-compare the local card with live recipe fields.
One batched RPC read merges Precision and Exploration from Chunk with Smelting XP from Game.
CACHED MATERIALS, LOW-COST HEAT
Chunk.js renders the workshop efficiently without turning visual data into authority
Smelted material records store compact identity and metadata, not geometry. Chunk.js maps a recognized material ID to a procedural low-poly builder, emits the shared chunk-object vertex format, collects its palette and finish, and caches both preview meshes and surface profiles with a forty-eight-entry bound. The cyan glass ingot, charcoal pieces, cloth folds, blooms, plates, composites, and dyes are therefore reconstructed client assets rather than bytes embedded in each Backpack record.
The furnace core is a small WebGL2 path, not a general scene. It draws one fullscreen triangle strip with a shader, disables antialiasing, depth, stencil, and buffer preservation, and caps pixel ratio. The play panel updates that visual on a throttled interval while a transaction is pending. Heat tier, progress, pulse, embers, and color communicate state economically, but the animation is not a timer enforced by the program.
The bundled temperatureC and burnSeconds values, chemistry composition ranges, derived attributes, roughness, translucency, emissive color, model description, and forge-use category never enter execute_smelting. The program recognizes fuel through hardcoded block or item identities, reads only minHeatTier and yieldBps from the supplied recipe, and writes normalized slot metadata. Keep UI copy explicit about this boundary so an attractive material preview is not mistaken for verified chemistry or provenance.
Repeated previews reuse generated geometry and surface profiles instead of rebuilding them per slot.
A capped canvas and disabled depth work keep the heat effect small.
Visual temperature, duration, composition, animation, and mesh fields remain local.
ADDRESS, RECORD, CAPACITY, AND YIELD EQUATIONS
Five equations expose where records move and which identities are enforced
The table equation binds a deterministic address, fixed byte budget, and fixed recipe authority. The matching equation allocates quantities from unique Backpack indexes. Capacity and yield equations model partial stack consumption and atomic output, while metadata normalization preserves table-backed output provenance.
Recipe, authority, and progress PDAs with fixed table length
P_table(id) = findProgramAddress([UTF8("smelting-recipes"), LE64(id)], smeltingProgramId); P_authority = findProgramAddress([UTF8("smelting-authority")], smeltingProgramId); P_progress(g,o) = findProgramAddress([UTF8("player-progress"), g, o], smeltingProgramId); R = 16 + 8*80 + 4*80 + 8 = 984 bytes; L = 96 + 10*R = 9936 bytes; authority_fixed = 9XuoVVwqP2jipt3jpJVXCSS2N2jr9vDuV3d6K73FKVudInitialization validates the table derivation for the requested 64-bit ID, requires the fixed recipe-authority signer, creates the 9,936-byte account, and stores that same fixed authority. Direct upsert requires the same signer. Execution validates the stored table ID, bump, PDA address, authority, and owner before reading a recipe. PlayerProgress uses the canonical Core GlobalConfig PDA g and owner o; both the GlobalConfig address and Core owner are pinned.
- P_table(id)
- One RecipeTable PDA under the active standalone or unified-game Smelting Program.
- R, L
- One fixed recipe record and the complete header-plus-ten-record account length.
- authority_fixed
- The fixed recipe-authority public key required for initialization and direct recipe upsert; no authority-transfer instruction exists.
- P_authority
- The trusted producer PDA that signs each Smelting output append into the Backpack.
- P_progress(g,o)
- The Smelting-program progress account derived from the canonical Core GlobalConfig and owner public keys.
Quantity allocation across unique Backpack indexes
m >= 1; allDistinct(concat(I,F)); required_j=quantity(template_j)*m; for each i in I: remaining_i=quantity(slot_i), consumed_i=0; for each matching j: take=min(remaining_i,required_j-matched_j), matched_j+=take, consumed_i+=take, remaining_i-=take; require consumed_i>0 and matched_j=required_j for every active jI and F contain current Backpack indexes, while required, matched, and consumed values count units. A selected material stack can satisfy several units of one template and can be partially retained. Block matching uses kind and Packed-Y block ID. Item matching uses kind, category, item code, and item ID when the template does not use zero as a wildcard. Flags, coordinates, PDA, volume, durability, grade, level, quality, and metadata do not change identity matching; their consumed physical share is handled after quantity allocation.
- m
- Unsigned batch multiplier carried by the execution payload.
- I, F
- Selected input-index and fuel-index arrays in the dense Backpack account.
- matched[j]
- Number of units allocated to recipe template j, compared with template quantity times multiplier.
Capacity-safe partial-stack transition
1 <= |I|, 0 <= |F|, |I|+|F| <= 99; releasedInputs=count(i in I where consumed_i=quantity_i); releasedFuel=count(f in F where quantity_f=1); n_after=n_before-releasedInputs-releasedFuel+o; require n_after <= c; fuelUnitsConsumed=|F|; require maxFuelTier(F) >= minHeatTierOnly fully consumed input records and one-unit fuel records release Backpack slots. A stacked input or stacked material fuel remains in place after its proportional physical state is updated. Ambient recipes may submit no fuel; heated recipes fail when the maximum selected fuel tier is below minHeatTier. The current UI permits up to ninety-eight input records so one fuel index still fits the ninety-nine-index chain payload. Any later failure rolls the complete Solana instruction back.
- n_before, n_after, c
- Occupied records before execution, projected occupied records after accounting for retained stacks, and Backpack capacity.
- o
- Recipe outputCount, constrained to one through four.
- 99
- Combined chain payload ceiling for selected input and fuel indexes; fuelCount may be zero for ambient recipes.
Per-output PDA volume, input ratio, skill factor, and batch result
V_in=sum(i in I)(proportionalVolume(slot_i,consumed_i)); V_recipe=sum(k in activeInputs)(unitTemplateVolume_k*quantity(template_k)); level=max{k in {0,...,10}|xp_before>=threshold[k]}; skillBps=min(15000,10000+500*level); V_base_j=floor(max(1,V_pda_j)*m*max(1,V_in)/max(1,V_recipe*m)); V_j=max(1,min(4294967295,floor(floor(V_base_j*yieldBps/10000)*skillBps/10000))); Q_primary_j=floor(quantity(output_j)*m*skillBps/10000); Q_merge_j=sum(i in I)(consumed_i)Fuel volume is excluded. Partial material input uses the same proportional volume that the Backpack Program removes. Each active output independently scales its PDA template volume by batch size, the actual-versus-expected input-volume ratio, recipe yield, and skill bonus. A primary output stack begins with synchronized yieldCount and scales with batch and skill. A lossless merge output preserves the total consumed quantity. OutputCount remains the number of appended records, while each record now carries its authoritative stack quantity.
- V_in, V_recipe, V_pda_j, V_j
- Selected input volume, one batch's expected template-input volume, output j's PDA template volume, and the independently calculated volume stored in output record j.
- yieldBps, skillBps
- Recipe and Smelting PlayerProgress factors, each divided by 10,000.
- yieldCount
- Bundled output count encoded into the output template's quantity field and used by the UI preview.
Output durability and item-level normalization
D_max_j = clamp(roundHalfUp(D_template_j*V_j/1000000),1,u32max); currentBps_j = clamp(floor(max(1,D_current_template_j)*10000/max(1,D_template_j)),1,10000); D_current_j = clamp(roundHalfUp(D_max_j*currentBps_j/10000),1,D_max_j); effective_j = floor(D_current_j*qualityBps_j/10000); level_calc_j = min(100,1+min(80,isqrt(floor(effective_j/25)))+min(20,floor(V_j/500000))); level_out_j = min(100,max(level_template_j,level_calc_j))For each material output j, the program writes the independently calculated V_j and output quantity, scales maximum and current durability, clamps grade to one through ten and quality to one through 10,000, and raises item level to at least the volume-and-durability result. Item code, item ID, flags, and metadata otherwise come from the table template. Output kind must be Item, category must be Material, and item PDA must equal the executing RecipeTable, making the table address explicit provenance in the stored slot.
- D_template, D_current_template
- Per-million-volume maximum durability and the template current-durability value used to derive currentBps.
- qualityBps, effective
- Clamped quality and the quality-adjusted current durability used for level calculation.
- roundHalfUp
- The implemented positive-integer rounding obtained by adding half the divisor before division.
Only the fixed recipe authority may initialize a RecipeTable
Rustprograms/nicechunk_smelting/src/lib.rs if !payer.is_signer || !payer.is_writable {
return Err(NicechunkSmeltingError::InvalidPayer.into());
}
validate_recipe_authority(payer.key)?;
if !recipe_table.is_writable {
return Err(NicechunkSmeltingError::InvalidWritableAccount.into());
}
require_key_eq(
system_program_account.key,
&system_program::ID,
NicechunkSmeltingError::InvalidSystemProgram,
)?;
let bump = validate_recipe_table_pda(program_id, recipe_table.key, table_id)?;
if recipe_table.owner == program_id {
return Err(NicechunkSmeltingError::RecipeTableAlreadyInitialized.into());
}
if recipe_table.owner != &system_program::ID || recipe_table.data_len() != 0 {
return Err(NicechunkSmeltingError::InvalidSystemAccount.into());
}
create_recipe_table_pda(
payer,
recipe_table,
system_program_account,
program_id,
table_id,
bump,
)?;
let clock = Clock::get()?;
let mut data = recipe_table.try_borrow_mut_data()?;
RecipeTable::pack_empty(
&mut data,
&RecipeTableInitArgs {
bump,
table_id,
authority: &NICECHUNK_SMELTING_RECIPE_AUTHORITY,
created_slot: clock.slot,
created_at: clock.unix_timestamp,
},
)
The initializer requires a writable signing payer and then compares that key with the compiled recipe authority. It validates the requested PDA, allocates the fixed table, and stores the same fixed authority rather than assigning control to an arbitrary payer.
Execution validates table identity, canonical GlobalConfig, and output provenance
Rustprograms/nicechunk_smelting/src/lib.rsfn execute_smelting(
program_id: &Pubkey,
accounts: &[AccountInfo],
payload: &[u8],
) -> ProgramResult {
if accounts.len() != 10 || payload.len() < 13 {
return Err(NicechunkSmeltingError::InvalidInstruction.into());
}
let recipe_id = read_u64(payload, 0);
let input_count = payload[8] as usize;
let fuel_count = payload[9] as usize;
let multiplier = read_u16(payload, 10);
let index_offset = 12;
// Ambient crafting recipes intentionally submit no fuel. Heated recipes
// are still rejected later when max fuel tier is below min_heat_tier.
if !smelting_payload_shape_is_valid(payload.len(), input_count, fuel_count, multiplier) {
return Err(NicechunkSmeltingError::InvalidInstruction.into());
}
let indexes = &payload[index_offset..index_offset + input_count];
let fuel_indexes = &payload[index_offset + input_count..];
let account_info_iter = &mut accounts.iter();
let owner = next_account_info(account_info_iter)?;
let recipe_table = next_account_info(account_info_iter)?;
let backpack = next_account_info(account_info_iter)?;
let player_progress = next_account_info(account_info_iter)?;
let global_config = next_account_info(account_info_iter)?;
let material_physics = next_account_info(account_info_iter)?;
let smelting_authority = next_account_info(account_info_iter)?;
let backpack_program = next_account_info(account_info_iter)?;
let player_skills = next_account_info(account_info_iter)?;
let system_program_account = next_account_info(account_info_iter)?;
if !owner.is_signer || !owner.is_writable {
return Err(NicechunkSmeltingError::InvalidPayer.into());
}
if !backpack.is_writable || !player_progress.is_writable {
return Err(NicechunkSmeltingError::InvalidWritableAccount.into());
}
require_key_eq(
system_program_account.key,
&system_program::ID,
NicechunkSmeltingError::InvalidSystemProgram,
)?;
require_key_eq(
recipe_table.owner,
program_id,
NicechunkSmeltingError::InvalidRecipeTableOwner,
)?;
require_key_eq(
backpack.owner,
&NICECHUNK_BACKPACK_PROGRAM_ID,
NicechunkSmeltingError::InvalidBackpackProgram,
)?;
require_key_eq(
backpack_program.key,
&NICECHUNK_BACKPACK_PROGRAM_ID,
NicechunkSmeltingError::InvalidBackpackProgram,
)?;
validate_global_config(global_config.key, global_config.owner)?;
validate_smelting_authority(program_id, smelting_authority.key)?;
let clock = Clock::get()?;
let progress_bump = validate_player_progress_pda(
program_id,
player_progress.key,
global_config.key,
owner.key,
)?;
create_player_progress_if_needed(
owner,
player_progress,
global_config,
system_program_account,
program_id,
owner.key,
progress_bump,
&clock,
)?;
let recipe_table_data = recipe_table.try_borrow_data()?;
RecipeTable::validate_identity(
&recipe_table_data,
program_id,
recipe_table.key,
&NICECHUNK_SMELTING_RECIPE_AUTHORITY,
)?;
let recipe = RecipeTable::find_recipe(&recipe_table_data, recipe_id)?;
recipe.validate_outputs_for_table(recipe_table.key)?;
drop(recipe_table_data);
Execution requires the final ten-account shape and validates GlobalConfig, RecipeTable identity, output provenance, PlayerProgress, and PlayerSkills before consuming inputs.
Selected stacks allocate exact recipe quantities and retain capacity honestly
Rustprograms/nicechunk_smelting/src/state.rs let mut matched_inputs = [0_u64; RECIPE_MAX_INPUTS];
let mut consumption_quantities = [0_u32; BACKPACK_MAX_CAPACITY];
let mut input_volume_mm3 = 0_u64;
let mut consumed_input_units = 0_u64;
let merge_recipe = recipe_is_material_merge(recipe);
if merge_recipe
&& (!fuel_indexes.is_empty()
|| indexes.len() < 2
|| multiplier as usize != indexes.len())
{
return Err(NicechunkSmeltingError::InputRecipeMismatch.into());
}
for index in indexes {
let selected = *index as usize;
if selected >= item_count || seen_indexes[selected] {
return Err(NicechunkSmeltingError::InvalidInputIndex.into());
}
seen_indexes[selected] = true;
let record = Self::slot_at(data, *index)?;
if merge_recipe {
if !recipe_input_matches(&recipe.inputs[0], &record) {
return Err(NicechunkSmeltingError::InputRecipeMismatch.into());
}
consumption_quantities[selected] = record.quantity;
consumed_input_units = consumed_input_units.saturating_add(record.quantity as u64);
input_volume_mm3 = input_volume_mm3.saturating_add(slot_volume_mm3(&record) as u64);
continue;
}
let mut remaining = record.quantity as u64;
for recipe_index in 0..recipe.input_count as usize {
let required =
(recipe.inputs[recipe_index].quantity as u64).saturating_mul(multiplier as u64);
if remaining == 0
|| matched_inputs[recipe_index] >= required
|| !recipe_input_matches(&recipe.inputs[recipe_index], &record)
{
continue;
}
let consumed = remaining.min(required.saturating_sub(matched_inputs[recipe_index]));
matched_inputs[recipe_index] =
matched_inputs[recipe_index].saturating_add(consumed);
consumption_quantities[selected] = consumption_quantities[selected]
.saturating_add(consumed.min(u32::MAX as u64) as u32);
remaining = remaining.saturating_sub(consumed);
}
let consumed = consumption_quantities[selected];
if consumed == 0 {
return Err(NicechunkSmeltingError::InputRecipeMismatch.into());
}
consumed_input_units = consumed_input_units.saturating_add(consumed as u64);
input_volume_mm3 = input_volume_mm3
.saturating_add(proportional_consumed_volume_mm3(&record, consumed)? as u64);
}
The validator counts units rather than records. It records the exact quantity to consume from each unique input index; the Backpack CPI then either removes the complete record or proportionally updates its remaining physical state.
The browser checks only that its derived table account has data
JavaScriptsrc/chain/nicechunkChain.js const [recipeTable] = deriveSmeltingRecipeTablePdaForContext(recipeTableId, context);
const recipeTableAccount = await conn.getAccountInfo(recipeTable, "confirmed");
if (!recipeTableAccount?.data?.length) {
return { submitted: false, reason: "smelting-table-uninitialized", recipeTable: recipeTable.toBase58() };
}
const smeltingProgress = deriveSmeltingPlayerProgressPdaForContext(provider.publicKey, context)[0];
const baselineInstruction = await createPlayerSkillsBaselineInstructionIfNeeded({
payer: provider.publicKey,
owner: provider.publicKey,
sourceAccounts: [smeltingProgress],
ruleIndexes: smeltingRuleIndexes,
connection: conn,
});
const tx = new Transaction();
tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }));
if (baselineInstruction) tx.add(baselineInstruction);
tx.add(createExecuteSmeltingInstruction({
owner: provider.publicKey,
recipeTable,
backpack,
recipeId,
inputIndexes: normalizedInputIndexes,
fuelIndexes: normalizedFuelIndexes,
batchMultiplier,
context,
}));
tx.add(createSyncPlayerSkillsInstruction({
payer: provider.publicKey,
owner: provider.publicKey,
sourceAccounts: [
smeltingProgress,
derivePlayerProfilePda(provider.publicKey)[0],
backpack,
],
}));
The website derives the bundled table and checks that it has data; the program performs canonical recipe validation. The transaction also appends PlayerSkills synchronization.
Chunk.js builds each registered material mesh once and bounds the cache
JavaScriptchunk.js/renderer/smelting-material-models.jsexport function createSmeltingMaterialPreviewMesh(options = {}) {
const materialId = normalizeMaterialId(options.materialId ?? options.id);
const definition = MODEL_DEFS[materialId];
if (!definition) return emptyMesh(materialId);
const cached = modelCache.get(materialId);
if (cached) return cached;
const vertices = [];
const indices = [];
definition.build(createBuilder(vertices, indices));
const colors = uniqueColors(vertices);
const mesh = {
id: `smelting_material_${materialId}`,
materialId,
name: definition.name,
category: "smelting material",
className: definition.className,
shape: definition.shape,
description: definition.description,
roughness: definition.roughness,
translucency: definition.translucency,
emissive: definition.emissive,
vertexFormat: "chunk-object",
vertices,
indices,
layers: [],
colors,
quadCount: indices.length / 6,
triangleCount: indices.length / 3,
vertexCount: vertices.length,
collision: false,
};
modelCache.set(materialId, mesh);
if (modelCache.size > MODEL_CACHE_LIMIT) modelCache.delete(modelCache.keys().next().value);
return mesh;
}
The material ID selects a low-poly Chunk.js definition and a cache limited to forty-eight entries. This reconstruction makes inventory and forge previews inexpensive, but no mesh, palette, roughness, translucency, emissive value, or triangle count is committed by the Smelting Program.
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_smelting/src/lib.rs
Dispatches fixed-authority table creation and upsert, execution, and Civilization receipt patches; pins canonical GlobalConfig, validates table identity and output provenance, performs owner-only execution, consumes exact input and fuel quantities, appends normalized outputs, and updates Smelting progress.
programs/nicechunk_smelting/src/state.rs
Defines the 9,936-byte version-2 table, fixed authority identity checks, ten 984-byte records, strict Material output provenance, quantity-aware input allocation, merge handling, fuel behavior, capacity projection, progress thresholds, and recipe validation.
programs/nicechunk_smelting/src/errors.rs
Enumerates distinct failures for recipe ownership and bytes, authority, missing recipes, input indexes, input mismatch, fuel heat, Backpack capacity, progress accounts, and CPI boundaries used by the described failure paths.
programs/nicechunk_smelting/src/civilization_adapter.rs
Validates finalized and executed Civilization rule-book and receipt state for a patch targeted at one recipe table, providing the alternate governance-mediated upsert route.
programs/nicechunk_smelting/src/cluster_config.rs
Binds the current build to Core, Backpack, and Civilization program IDs plus the fixed recipe authority.
programs/nicechunk_game/src/lib.rs
Routes the unified Game Program's Smelting namespace to the same Smelting processor, explaining the active shared program ID without changing RecipeTable semantics.
programs/nicechunk_backpack/src/lib.rs
Authenticates the smelting-authority PDA before appending a supplied 80-byte output slot and before applying quantity-aware Smelting consumption.
programs/nicechunk_backpack/src/state.rs
Implements strict item append, exact partial-stack Smelting consumption, proportional volume and durability retention, MaterialPhysics mass recomputation, Backpack total-mass accounting, dense removal, and atomic capacity behavior.
sdk/nicechunk-smelting.ts
Mirrors fixed recipe authority, table and producer seeds, fixed lengths, strict table-backed Material outputs, execution multiplier, canonical Core progress derivation, and instruction account order; it exposes no authority-transfer builder.
src/chain/nicechunkChain.js
Derives configured recipe tables, checks account existence before submission, builds owner-signed execution, and batches Chunk plus Game/Smelting PlayerProgress reads with strict owner, version, bump, owner-key, and GlobalConfig validation.
src/data/smeltingRules.js
Provides bundled heat temperatures, fuel duration and consumable flags, local recipes, yieldCount, composition, identity mapping, property previews, exact local ratio matching, and 220/320-series table conventions that are not themselves decoded from chain state.
play/smelting-rules-lite.js
Adapts Backpack slots to local rules, plans requirements by stack quantity, constructs lossless merge candidates, and exports chain identity selection that preserves primary versus merge routing.
play/play-smelting.js
Caps input selection at ninety-eight, chooses one fuel slot, displays and previews quantity-aware output, submits bundled identities, refreshes confirmed Backpack state, and throttles the furnace visual.
play/play-chain-player.js
Loads the generic fetched PlayerProgress into visible skill XP, including the smelting field used by local skill effects.
play/play-chain-backpack.js
Reconstructs known material item codes, falls back to material-code labels for unknown outputs, and derives rich preview attributes locally rather than decoding a complete authoritative property profile from the slot.
play/play-skill-effects.js
Turns the UI's visible Smelting level into a 10,000-plus-500-per-level output multiplier, capped at 15,000 basis points.
scripts/sync-smelting-recipes.ts
Writes one quantity-bearing template per bundled requirement, stores yieldCount in every primary output, writes lossless quantity-one merge templates, retains disabled recipe records, derives output metadata locally, and distributes recipes across fixed-capacity tables.
public/mainnet.json
Declares the current website's Devnet Smelting Program, rule-set URL, configured table IDs and addresses, including the table-220 address used for the explicitly dated RPC audit.
chunk.js/renderer/smelting-material-models.js
Builds registered material meshes and shared visual surface profiles from code, returns the chunk-object format, and bounds mesh and profile caches to forty-eight entries.
chunk.js/renderer/smelting-core.js
Implements the low-cost WebGL2 furnace core with one fullscreen strip, capped pixel ratio, disabled depth and antialiasing, shader-driven heat and progress, and explicit resource disposal.
