Author carried bulk matter and sample-based assays
verifiedAgainst 971c93c · verifiedOn 2026-09-10.
What you will build
Section titled “What you will build”In typical voxel sandboxes, inventory treats all items as discrete, uniform tokens: 64 “Iron Ore” blocks or 16 “Crushed Malachite” items. In Voxamine, granular materials (ores, crushed concentrates, powders, metal ingots, and chemical solutions) are bulk matter (MASTERPLAN.md §30.4): they are carried as mass with composition, not as counts.
You will implement the bulk matter pipeline using MaterialBatch.cs, model conserved-by-construction operations (MergedWith and TrySplit), track deposit provenance via OriginId, enforce honest sample-based assays where destructive methods consume their sample and non-destructive methods return it via MaterialBatchAssay.cs, ensure atomic inventory transitions via CarriedOreAssay.TryRun, and charge carried bulk matter into chemical reaction vessels via MaterialBatchCharging.cs.
// Split a real 20g sample from a 100g carried ore batch:if (CarriedOreAssay.TryRun( playerInventory, malachiteSubstanceId, AssayMethod.FireAssay, sampleGrams: 20d, sampleId: 101UL, out AssayReading reading, out MaterialBatch remainingBatch, out string error)){ // Destructive fire assay consumes the 20g sample: // remainingBatch.TotalMassGrams -> 80g // reading.MeasuredGrade01 -> 0.30 ± 0.03 // Proportional sampling ensures remaining grade is unchanged!}Where this sits
Section titled “Where this sits”Bulk matter bridges spatial mining and harvesting, player carrying capacity, instrument inspection, and industrial metallurgy:
- Inventory Core (
MaterialBatch&MaterialComponent) provides the immutable representation of mass and chemical composition with built-in atom conservation. - Analytical Layer (
MaterialBatchAssay&MaterialBatchNarrator) isolates physical samples, executes instrument measurements, and formats qualitative or quantitative readouts without revealing unmeasured chemistry. - Player Interaction (
CarriedOreAssay) guarantees that the inventory never temporarily owns two versions of the same material during an analytical test. - Vessel Charging (
MaterialBatchCharging) preflights vessel capacity and converts phase-agnostic mass (grams) into discrete chemical moles within aVesselCharge.
flowchart TD
subgraph MINING["World & Harvesters"]
MINED["Mined Block / Harvested Plant\n(Measured Mass & Substance Ratios)"]
BATCH["MaterialBatch\n(Immutable: SubstanceId -> MassGrams, OriginId)"]
end
subgraph INVENTORY["Player Carrying Layer"]
PLAYER["PlayerInventory.BulkMatter\n(Carried matter as mass, not count)"]
MERGE["MaterialBatch.MergedWith\n(Sums masses; keeps OriginId if identical)"]
SPLIT["MaterialBatch.TrySplit\n(Proportional split: preserves mass fractions)"]
end
subgraph ASSAY["Analytical Assay Engine"]
RUN["CarriedOreAssay.TryRun\n(Single-owner inventory transaction)"]
ASSAY_METH["MaterialBatchAssay.TryAssay\n(Takes sampleGrams, seeds scatter)"]
CONSUME{"Method Consumes\nSample?"}
FIRE["FireAssay: Sample Consumed\nRemaining = Parent - Sample"]
RETURN["Density / Hand: Sample Returned\nRemaining = Remainder + Sample"]
end
subgraph INDUSTRY["Vessel Reaction System"]
CHARGE["MaterialBatchCharging.TryChargeIntoVessel\n(Preflights capacity, converts g -> moles)"]
VESSEL["VesselCharge\n(Thermodynamic chemical reaction solver)"]
end
MINED --> BATCH
BATCH --> PLAYER
PLAYER --> MERGE
PLAYER --> SPLIT
PLAYER --> RUN
RUN --> ASSAY_METH
ASSAY_METH --> CONSUME
CONSUME -->|Yes (Smelted)| FIRE
CONSUME -->|No (Displaced)| RETURN
FIRE --> PLAYER
RETURN --> PLAYER
PLAYER --> CHARGE
CHARGE --> VESSEL
Before you start
Section titled “Before you start”Read Docs/MASTERPLAN.md §30.4 (“Bulk matter”), §30.5 (“Assays and evidence”), and §36.4 (“Mined-block to vessel charging”), then inspect:
MaterialBatch, the immutable compositional-truth layer.MaterialBatchAssay, destructive and non-destructive sample evaluation.CarriedOreAssay, the atomic player-inventory transaction.MaterialBatchCharging, all-or-nothing molar transfer into reaction vessels.MaterialBatchTests, edit-mode tests asserting mass and atom conservation.
Review the governing principles of bulk matter:
| Principle | Rule | Implementation |
|---|---|---|
| Mixtures stay mixtures | Combining two granular materials never spawns a new item name. It creates a single batch with summed component masses. | MaterialBatch.MergedWith |
| Proportional splitting | Splitting a sample off a batch preserves exact mass fractions: sampling cannot enrich or impoverish the parent. | MaterialBatch.TrySplit |
| Provenance tracking | A batch tracks where it came from via OriginId. Merging two batches from the same vein preserves origin; mixing different veins resets origin to 0. |
OriginId = (a.OriginId == b.OriginId) ? a.OriginId : 0UL |
| No hidden omniscience | An unassayed batch exposes total mass to the UI, but never percentages or chemical identities. | MaterialBatchNarrator.Describe |
| Sample destruction | A fire assay destroys the sample it measures; Archimedes density testing returns it to the batch. | MaterialBatchAssay.MethodConsumesSample |
The build, step by step
Section titled “The build, step by step”1. The Immutable MaterialBatch Structure
Section titled “1. The Immutable MaterialBatch Structure”In MaterialBatch.cs, components are stored sorted by SubstanceId with strictly positive mass ($m > 0$):
public sealed class MaterialBatch{ private readonly MaterialComponent[] components;
public double TotalMassGrams { get; } public ulong OriginId { get; } public int ComponentCount => components.Length;
public double MassOf(SubstanceId substanceId) { int index = BinarySearch(substanceId); return index >= 0 ? components[index].MassGrams : 0d; }
public double MassFractionOf(SubstanceId substanceId) { return TotalMassGrams > 0d ? MassOf(substanceId) / TotalMassGrams : 0d; }}2. Conserved-by-Construction Merge and Split
Section titled “2. Conserved-by-Construction Merge and Split”Merging two batches sums component masses and evaluates provenance:
public MaterialBatch MergedWith(MaterialBatch other){ if (other == null || other.IsEmpty) return this; if (IsEmpty) return other;
// Sum masses across sorted arrays (linear merge): var mergedComponents = MergeSortedComponents(this.components, other.components); double totalMass = this.TotalMassGrams + other.TotalMassGrams; ulong origin = (this.OriginId == other.OriginId) ? this.OriginId : 0UL;
return new MaterialBatch(mergedComponents, totalMass, origin);}Splitting divides each component strictly proportionally:
public bool TrySplit(double splitMassGrams, out MaterialBatch split, out MaterialBatch remainder, out string error){ if (splitMassGrams <= 0d || splitMassGrams >= TotalMassGrams) { error = "Split mass must be strictly between 0 and total batch mass."; return false; }
double ratio = splitMassGrams / TotalMassGrams; var splitParts = new MaterialComponent[components.Length]; var remParts = new MaterialComponent[components.Length];
for (int i = 0; i < components.Length; i++) { double partMass = components[i].MassGrams * ratio; splitParts[i] = new MaterialComponent(components[i].SubstanceId, partMass); remParts[i] = new MaterialComponent(components[i].SubstanceId, components[i].MassGrams - partMass); }
split = new MaterialBatch(splitParts, splitMassGrams, OriginId); remainder = new MaterialBatch(remParts, TotalMassGrams - splitMassGrams, OriginId); return true;}Because every component mass scales by ratio and 1 - ratio, total mass and individual atomic counts are conserved exactly.
3. Destructive and Non-Destructive Assays
Section titled “3. Destructive and Non-Destructive Assays”In MaterialBatchAssay.TryAssay, the assay takes an actual sample from the parent batch:
public static bool TryAssay( MaterialBatch batch, SubstanceId targetSubstanceId, AssayMethod method, double sampleGrams, ulong sampleId, out AssayReading reading, out MaterialBatch remainingBatch, out string error){ if (!batch.TrySplit(sampleGrams, out MaterialBatch sample, out MaterialBatch remainder, out error)) { return false; }
float trueFraction = (float)sample.MassFractionOf(targetSubstanceId); reading = OreAssayMath.Assay(trueFraction, method, sampleId);
// Destructive methods discard the sample; non-destructive ones re-merge it: remainingBatch = MethodConsumesSample(method) ? remainder : remainder.MergedWith(sample); return true;}4. Single-Owner Inventory Hand-off
Section titled “4. Single-Owner Inventory Hand-off”In CarriedOreAssay.TryRun, player inventory is updated only after all fallible operations succeed:
public static bool TryRun( PlayerInventory inventory, SubstanceId targetSubstanceId, AssayMethod method, double sampleGrams, ulong sampleId, out AssayReading reading, out MaterialBatch assayedBatch, out string error){ MaterialBatch before = inventory.BulkMatter; if (!MaterialBatchAssay.TryAssay(before, targetSubstanceId, method, sampleGrams, sampleId, out reading, out MaterialBatch after, out error)) { return false; }
// Atomic commit: one removal followed by insertion: inventory.RemoveBulk(before); inventory.AddBulk(after); assayedBatch = inventory.BulkMatter; return true;}5. Phase-Aware Vessel Charging
Section titled “5. Phase-Aware Vessel Charging”In MaterialBatchCharging.TryChargeIntoVessel, convert grams into moles using the vessel’s SubstanceTable:
$$n_i = \frac{m_i}{M_i}$$
for (int index = 0; index < batch.ComponentCount; index++){ MaterialComponent component = batch.ComponentAt(index); ThermodynamicPhaseDefinition def = substances.Get(component.SubstanceId, phase); double moles = component.MassGrams / def.MolarMassGramsPerMol;
molesToAdd[index] = moles; if (!charge.Contains(component.SubstanceId, phase)) { newKeys.Add(component.SubstanceId.Value); }}
// Preflight: ensure charge.KeyCount + newKeys.Count <= charge.Capacityif (charge.KeyCount + newKeys.Count > charge.Capacity){ error = "Vessel does not have enough capacity for all batch components."; return false;}
// Apply atomically:for (int i = 0; i < batch.ComponentCount; i++){ charge.AddMoles(batch.ComponentAt(i).SubstanceId, phase, molesToAdd[i]);}Verify
Section titled “Verify”1. Execute Headless EditMode Tests
Section titled “1. Execute Headless EditMode Tests”Run the focused Inventory and UI test fixtures:
/opt/unity/Editor/Unity -batchmode -nographics \ -projectPath /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD \ -runTests -testPlatform EditMode \ -testFilter VoxelSandbox.Tests.MaterialBatchTests/opt/unity/Editor/Unity -batchmode -nographics \ -projectPath /home/soulwax/workspace/engines/unity/minecraft/Minecraft-HD \ -runTests -testPlatform EditMode \ -testFilter VoxelSandbox.Tests.CarriedOreAssayTestsConfirm that:
TryCreate_TotalsTheComponentMassesAndReportsFractionspasses with exact floating-point tolerances.MergedWith_SumsEverySubstanceAndConservesTotalMassverifies mass and atom conservation.HandSpecimen_ReturnsTheSampleAndOnlyThenMakesAQualitativeGradeLegibleconfirms non-destructive sample recovery.ConsumingMethod_ChangesOnlyTheSampleMassAndKeepsTheRemainderconfirms destructive sample loss without enriching or impoverishing the remaining ore.
Now do your own
Section titled “Now do your own”To implement a new bulk matter flow (e.g. Crushed Quartz Sand or Kiln Wood Ash):
- Author Substance Components: Ensure the mineral or chemical species exist in
SubstanceCatalog.asset. - Instantiate via
MaterialBatch.TryCreate: Supply sorted component masses and validoriginId. - Verify Conserved Transitions: Route bulk drops through
MaterialBatchDropRuntimewhen dropped in the world. - Wire Vessel Charging: Use
MaterialBatchCharging.TryChargeIntoVesselto load bulk matter into crucibles or leaching vats. - Add Regression Tests: Assert mass conservation across merge/split cycles in
MaterialBatchTests.cs.
Pitfalls
Section titled “Pitfalls”| Symptom | Cause | Fix |
|---|---|---|
| Granular materials turn into integer stack counts (e.g. “64 Sand”). | Granular matter was routed through ItemStack instead of MaterialBatch. |
Use MaterialBatch for granular matter, powders, solutions, and bulk ores. Reserve ItemStack strictly for countable tools and apparatus. |
| Ore grade increases or decreases after splitting a sample. | Split logic divided some components unequally. | Always scale every component by the identical mass ratio $r = m_{\text{split}} / m_{\text{total}}$ as implemented in MaterialBatch.TrySplit. |
| Unassayed ore in inventory shows exact chemical percentages in the HUD. | UI accessed batch.MassFractionOf() directly without an AssayReading. |
Never expose raw mass fractions to the player. Format descriptions through MaterialBatchNarrator.Describe(batch, reading). |
| Vessel charge partially applies before failing due to capacity limits. | Components were loaded one by one into VesselCharge without preflighting. |
Use MaterialBatchCharging.TryChargeIntoVessel; it counts distinct new keys and validates total vessel capacity before applying any moles. |
| Merged batch retains an incorrect origin ID after mixing two different deposits. | Merge logic blindly copied a.OriginId. |
Enforce origin validation: origin = (a.OriginId == b.OriginId) ? a.OriginId : 0UL. |
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.