Author a quantitative ore assay and vessel charge profile
verifiedAgainst 05ea923 · verifiedOn 2026-09-10.
What you will build
Section titled “What you will build”In Voxamine, mining an ore block does not yield a pure, magical metal nugget. An ore block is an authentic geological composite composed of primary mineral phases and inert waste rock (gangue). Furthermore, natural deposits exhibit continuous grade gradients: deposit fringes are lean, while deep vein cores are rich (MASTERPLAN.md §30.3 / §36.4).
You will author a complete mineral assay and vessel charging profile for an ore block by defining its BlockCompositionComponent entries, evaluating grade-aware chemical yields in BlockCompositionCharging, modeling realistic instrument error bars across visual hand inspection, hydrometer density, and fire assaying via OreAssayMath, and auditing the resulting vessel charge inside the Assay Lab Voxel Workshop module (AssayLabModule.cs).
// Grade-aware charging from mined coordinate into a chemical vessel:double grade = OreGradeMath.GradeAt(worldX, worldY, worldZ, settings);BlockCompositionCharging.TryChargeIntoVessel(malachiteBlock, count: 4, grade, vesselCharge, out string error);// Yields proportional moles of Cu2CO3(OH)2(s) and inverse moles of SiO2(s) gangue!Where this sits
Section titled “Where this sits”The ore assay and charging pipeline connects spatial world generation, authored block definitions, pure chemistry calculations, and industry vessels:
- World Generation (
OreGradeMath) computes continuous 3D noise yielding an authoritative ore grade $g \in [0, 1]$ at the mined coordinate. - Data Blocks (
BlockDefinition.Composition) pairs each block with its authored substances, molar base quantities, and component roles (PrimaryOre,Gangue, orFixed). - Charging Service (
BlockCompositionCharging) converts mined blocks at grade $g$ into discrete chemical moles within aVesselCharge. - Measurement Engine (
OreAssayMath&OreAssayNarrator) evaluates what the player’s instruments can detect: qualitative eyeball bands, density approximations, or quantitative fire assays. - Voxel Workshop (
AssayLabModule) provides an interactive authoring scratchpad to inspect yields, slide grade values, and verify narratives.
flowchart TD
subgraph VOXEL["World & Mining"]
COORD["Mined Block Coordinate\n(X, Y, Z)"]
GRADE["OreGradeMath.GradeAt\n(Continuous grade g ∈ [0, 1])"]
end
subgraph DEFINITION["Assets/_Game/Data/Blocks/"]
BLOCK["BlockDefinition\n(Malachite Ore, Cassiterite, etc.)"]
COMP_ORE["BlockCompositionComponent\n(Role = PrimaryOre, base moles)"]
COMP_GANGUE["BlockCompositionComponent\n(Role = Gangue, base moles)"]
end
subgraph CHARGE_PATH["VoxelSandbox.Data.Blocks"]
CHARGER["BlockCompositionCharging\nYieldMolesAt(grade, count)"]
VESSEL["VesselCharge\n(Immutable moles of substance & phase)"]
end
subgraph ASSAY_PATH["VoxelSandbox.Chemistry"]
INSTRUMENTS["AssayMethod\n(HandSpecimen, Density, FireAssay)"]
ASSAY_MATH["OreAssayMath.AssaySample\n(Scattered measurement ± uncertainty)"]
NARRATOR["OreAssayNarrator.Describe\n(Plain-language science text)"]
end
subgraph WORKSHOP["Voxel Workshop"]
LAB["AssayLabModule\n(Block picker, grade slider, charge audit)"]
end
COORD --> GRADE
BLOCK --> COMP_ORE
BLOCK --> COMP_GANGUE
COMP_ORE --> CHARGER
COMP_GANGUE --> CHARGER
GRADE --> CHARGER
CHARGER --> VESSEL
GRADE --> ASSAY_MATH
INSTRUMENTS --> ASSAY_MATH
ASSAY_MATH --> NARRATOR
BLOCK --> LAB
CHARGER --> LAB
NARRATOR --> LAB
Before you start
Section titled “Before you start”Read Docs/MASTERPLAN.md §30.3 (“Ore grading and assays”) and §36.4 (“Mined-block to vessel charging”), and inspect:
BlockCompositionCharging, all-or-nothing molar transfer into vessel charges.BlockCompositionComponent, defining component roles and yield functions.OreAssayMath, the measurement scatter and uncertainty model.OreAssayNarrator, human-readable readout formatting.AssayLabModule, the editor preview module.
Review the three assay instrument tiers:
| Method | Instrument | Output Type | Error Bracket | Example In-Game Readout |
|---|---|---|---|---|
HandSpecimen |
Bare hands / eye | Qualitative (Lean, Middling, Rich) |
Full band | “By eye and weight, this looks like middling ore.” |
Density |
Archimedes balance | Quantitative percentage | $\pm 15%$ | “The balance reads about 48% grade, give or take 15 points.” |
FireAssay |
Cupel furnace | Quantitative percentage | $\pm 3%$ | “The fire assay reads about 52% grade, give or take 3 points.” |
The build, step by step
Section titled “The build, step by step”1. Authoring Block Composition Components
Section titled “1. Authoring Block Composition Components”An ore block must declare its mineral constituents using BlockCompositionComponent. A copper carbonate block (Malachite) contains malachite as PrimaryOre and quartz/calcite as Gangue:
// Authoring a Malachite block composition:block.Composition = new List<BlockCompositionComponent>{ // Primary ore scales directly with grade g: moles = baseMoles * g * count new BlockCompositionComponent( substance: malachiteSolid, molesPerBlock: 120.0, role: BlockComponentRole.PrimaryOre),
// Gangue scales inversely with grade: moles = baseMoles * (1 - g) * count new BlockCompositionComponent( substance: quartzSolid, molesPerBlock: 350.0, role: BlockComponentRole.Gangue)};When grade is $1.0$ (pure vein core), gangue yields $0\text{ mol}$; when grade is $0.1$ (lean fringe), gangue dominates the charge, requiring the player to smelt mostly flux and slag.
2. All-or-Nothing Grade-Aware Vessel Charging
Section titled “2. All-or-Nothing Grade-Aware Vessel Charging”Inspect BlockCompositionCharging.TryChargeIntoVessel:
public static bool TryChargeIntoVessel( BlockDefinition block, int blockCount, double grade01, VesselCharge charge, out string error){ // 1. Preflight capacity and distinct new key additions var newKeys = new HashSet<(int SubstanceId, ChemicalPhase Phase)>(); for (int index = 0; index < block.Composition.Count; index++) { BlockCompositionComponent component = block.Composition[index]; double deltaMoles = component.YieldMolesAt(grade01, blockCount); if (deltaMoles <= 0d) continue;
if (!charge.CanAdjustMoles(component.Substance.SubstanceId, component.Substance.Phase, deltaMoles, out error)) { return false; } if (!charge.Contains(component.Substance.SubstanceId, component.Substance.Phase)) { newKeys.Add((component.Substance.SubstanceId.Value, component.Substance.Phase)); } }
if (charge.Count + newKeys.Count > charge.Capacity) { error = "Vessel charge is full."; return false; }
// 2. Commit all molar additions atomically for (int index = 0; index < block.Composition.Count; index++) { BlockCompositionComponent component = block.Composition[index]; double deltaMoles = component.YieldMolesAt(grade01, blockCount); if (deltaMoles > 0d) { charge.AdjustMoles(component.Substance.SubstanceId, component.Substance.Phase, deltaMoles); } }
error = null; return true;}If a vessel cannot hold both the ore mineral and the accompanying gangue waste rock, charging fails completely before any substance is added.
3. Sampling the Measurement Ladder
Section titled “3. Sampling the Measurement Ladder”In OreAssayMath.AssaySample, evaluate the instrument reading:
public static AssayReading AssaySample(float trueGrade01, AssayMethod method, int sampleId){ trueGrade01 = Mathf.Clamp01(trueGrade01);
if (method == AssayMethod.HandSpecimen) { // Qualitative band call AssayGradeBand band = trueGrade01 switch { < 0.30f => AssayGradeBand.Lean, > 0.65f => AssayGradeBand.Rich, _ => AssayGradeBand.Middling }; return new AssayReading(trueGrade01, 0.5f, method, band); }
// Quantitative measurements add bounded pseudo-random scatter float uncertainty = method == AssayMethod.FireAssay ? 0.03f : 0.15f; float scatter = DeterministicValueNoise.HashToUnitFloat((uint)sampleId) * 2f - 1f; float measured = Mathf.Clamp01(trueGrade01 + scatter * uncertainty * 0.75f);
return new AssayReading(measured, uncertainty, method, AssayGradeBand.Middling);}The measurement is honest: the reported bracket [measured - uncertainty, measured + uncertainty] is mathematically guaranteed to enclose the true geological grade.
4. Plain-Language Narrative Generation
Section titled “4. Plain-Language Narrative Generation”OreAssayNarrator.Describe turns the reading into plain scientific English at the player’s reading level:
public static string Describe(in AssayReading reading){ if (!reading.IsQuantitative) { return reading.Band switch { AssayGradeBand.Lean => "By eye and weight, this is lean ore — most of it is waste rock.", AssayGradeBand.Rich => "By eye and weight, this looks like rich ore.", _ => "By eye and weight, this looks like middling ore.", }; }
int centre = Percent(reading.MeasuredGrade01); int give = Percent(reading.UncertaintyHalfWidth01); string instrument = reading.Method == AssayMethod.FireAssay ? "fire assay" : "balance"; return $"The {instrument} reads about {centre}% grade, give or take {give} points.";}5. Interactive Inspection via Assay Lab
Section titled “5. Interactive Inspection via Assay Lab”Open AssayLabModule.cs. The module lets designers select a block, adjust a grade slider from $0.0$ to $1.0$, choose block batch counts, and instantly inspect:
- Moles of primary ore and gangue generated.
- Simulated vessel charge capacity usage.
- Comparative readouts across all three instrument methods.
Verify
Section titled “Verify”1. Execute EditMode Test Fixtures
Section titled “1. Execute EditMode Test Fixtures”Run the focused Chemistry and Data EditMode fixtures headlessly:
/opt/unity/Editor/Unity -batchmode -nographics \ -projectPath /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD \ -runTests -testPlatform EditMode \ -testFilter VoxelSandbox.Tests.OreAssayMathTestsVerify that:
TrueGradeAlwaysFallsWithinReportedErrorBracketpasses across all test grades.HandSpecimenResolvesCorrectQualitativeBandsverifies lean ($< 30%$) and rich ($> 65%$).FireAssayProducesTighterUncertaintyThanDensityBalanceholds strictly.BlockCompositionChargingTestsproves full-batch atomic rollback when vessel capacity is exceeded.
2. Inspect in Assay Lab
Section titled “2. Inspect in Assay Lab”- Open Tools → Voxel Sandbox → Voxel Workshop → Assay Lab.
- Select Malachite Ore from the dropdown.
- Slide Grade to
0.20: confirm the Hand Specimen narrative reads “lean ore — most of it is waste rock”, and gangue moles dominate the charge. - Slide Grade to
0.85: confirm the Hand Specimen switches to “rich ore”, and primary copper carbonate moles increase dramatically. - Inspect the Fire Assay readout: verify it reports $\pm 3$ points uncertainty.
Now do your own
Section titled “Now do your own”To author a brand-new mineral ore block (e.g. Galena / Lead Sulfide or Bauxite):
- Author
SubstanceDefinitions: Ensure both the primary mineral (LeadSulfideSolid, $PbS$) and gangue (SilicaSolid, $SiO_2$) exist inSubstanceCatalog.asset. - Assign composition in
BlockDefinition: AddBlockCompositionComponents specifying base moles and setting roles toPrimaryOreandGangue. - Audit in Assay Lab: Open Assay Lab, select the new block, and verify that sliding grade between $0$ and $1$ smoothly shifts the molar proportion.
- Add EditMode tests: Add test cases in
BlockCompositionChargingTests.csvalidating batch charging and capacity limits.
Pitfalls
Section titled “Pitfalls”| Symptom | Cause | Fix |
|---|---|---|
| Vessel receives pure metal when smelting raw ore. | The ore block was authored without a Gangue component. |
Real ores contain waste rock. Always author matching gangue (SilicaSolid or CalciteSolid) with BlockComponentRole.Gangue. |
| Moles added to vessel do not change when mining different parts of an ore body. | The mining runtime called the grade-unaware overload TryChargeIntoVessel(block, count, charge) ($g=1.0$). |
Always pass the sampled coordinate grade: TryChargeIntoVessel(block, count, grade01, charge, out error). |
| Partial moles remain in a vessel when a charge fails partway through. | Components were adjusted sequentially without preflighting overall charge capacity. | Use BlockCompositionCharging.TryChargeIntoVessel; it verifies all new substance keys fit within charge.Capacity before adjusting values. |
| Hand specimen displays a false-precise percentage (e.g. “43.2% grade”). | AssayReading.IsQuantitative was bypassed in UI presentation. |
Qualitative methods must only report qualitative bands (Lean, Middling, Rich). Never print decimal percentages from unmeasured estimates. |
| Moles scale non-linearly with block count. | YieldMolesAt was computed with an exponent instead of linear molar scaling. |
Moles must scale linearly with count: $\Delta n = n_{\text{base}} \times g \times \text{count}$. |
User-contributed notes
Corrections, clarifications, and practical tips for this page. Anonymous is fine — a name is optional. Basic Markdown works:
**bold**,*italic*,`code`, and links.Notes policy
Notes are lightly filtered for spam and may be edited or removed. Keep them about this page — no support requests, no personal data, nothing you would not publish. Links are limited and marked
nofollow.