> **Superseded in part (master `df7c44f2`).** Chunk reach and picking no longer go through `Zone.ChunksNear` or an `ActionGameManager` candidate list. `Zone.Targets` (`ServerShared/Targeting/`) answers range and best-first queries, `Entity` owns the reticle, next, previous and nearest rules, and belt tasks, `SettleBelts`, `BeltUpdates` and `MineAsteroid` are gone. Read the as-built section "Mining index: as built" in `docs/aetheria-release-map.md` (branch `eureka/aetheria-release-target`) before any anchor below that names `ChunksNear`, `VisibleChunksInReach`'s old body, `ActionGameManager` target code or belt threading.
| **Chunk** | `ChunkId = (CultRecordKey Field, int Index)`. `Field` is the field's record key in the run store (today always an `AsteroidBeltData`; debris fields are another kind, Q2); `Index` indexes its chunks (`Asteroids` for a belt). Stable across save/continue, because `RunSave.Commit` writes the run store's whole view, bodies included (`SavedGame.cs:97-99`), and nothing rewrites `Asteroids` after generation. | Born at zone generation (`ZoneGenerator.cs:141-153`). Lives for the run. Removed with every `BodyData` by `RunSave.Clear` (`SavedGame.cs:124`). | `ZoneGenerator` writes `Asteroids` once. Nothing else may write it. |
| **Chunk pose** (position, rotation, velocity) | Derived: `(ChunkId, zone time)`. | Never stored. Evaluated on demand. | One function on `AsteroidBelt` (Cut 1). Forbidden writers: any stored pose array, any per-tick loop. |
| **Chunk wear** (damage taken, broken-until time) | Keyed by `ChunkId` inside the owning `Zone`. | Damage accumulates at Apply. When it reaches the chunk's hitpoints, the chunk breaks with `RespawnAt = zone time + AsteroidRespawnTime(size)`. At `time >= RespawnAt` it is whole again, read lazily with no timer. Persisted in `ZonePack` (new nullable key 6) with the zone's own `Time`, and pruned of expired entries at pack. | `FireControl.Apply` through one `Zone` method is the only writer. `Zone` owns the storage. |
| **Chunk composition** | Derived: `(belt identity, ChunkId, catalog ore table)`. | Never stored (recommended, Q7). A retune of the catalog retunes every belt. | The catalog author, through per-ore data on `SimpleCommodityData`. The derivation function is the only reader of that data for belts. |
| **Ore affinity and abundance** | Per ore: new nullable fields on `SimpleCommodityData` (keys 12 and 13 are free. Only key 11 is retired on `SimpleCommodityData`; keys 6, 7 and 8 are live, inherited from `ItemData` (`SpecificHeat`, `Conductivity`, `Price`, `ItemData.cs:291-298` @`b66ba524`). The comment at `ItemData.cs:304` wrongly lists 6, 7, 8 and 11). | Authored in Studio. Changes with the catalog. | Catalog author. |
| **Global loot tunables** | `GameplaySettings` / `PlanetSettings` fields (Cut 5). | Authored. Today they live in `Settings.asset`. They move with the settings-globals campaign (`docs/settings-globals-cut.md`, mapped, not landed). | Catalog author. |
| **Mined lot** | `LotId` in the run's `ProvenanceLedger`. `Lot { Design = the commodity, Origin = Extracted { Zone, Commodity, Body }, Quality, Roles = null }`. | Minted at the first extraction from a (zone index, belt, commodity) source (Q6). Reused for later units from that source. Collected by `Reachable` when no instance reaches it. A later extraction after collection mints a fresh lot. | `ItemManager.ExtractedLot(zone, belt, commodity)` is the only writer. Its source→lot index is derived at load, not persisted. |
| **Mined unit** | A `SimpleCommodity` instance, `Data` = commodity, `Lot` = the source lot (`Lot` moves to the `ItemInstance` base, key 11). | Stored in cargo. Stacks merge only within the same `Data` and `Lot`. A split copies `Lot`. | `EquippedCargoBay` owns placement. The mint owns `Lot`. |
| **Current target** | `Entity.Target` (runtime; not packed, `EntitySerializer.cs` has no target field **(read)**). | Set by targeting inputs or AI state. Cleared by the existing entity-removal paths. A broken chunk stays held and prices 0 until retargeted (Cut 3). | Player: the `ActionGameManager` targeting handlers. AI: the Minion states. Both write the one slot. |
| **Mining shot** | `ShotId` (`Zone.NextShotId`), as every shot. | Fire → Commit → arrival. Never serialized (`FireControl.cs:852-855`). | `FireControl`. |
No cell is empty. The cells marked "recommended" depend on the rulings below and are rewritten if a ruling goes the
other way.
---
## Operator questions, in the order they block cuts
The 2026-09-30 rulings on Q4-Q13 are recorded in the Part III status header. Where a question below was written before its ruling, the ruling wins and the recommendation text is history.
**Detection (operator correction, 2026-09-30, replacing the 2026-09-25 "deferred to the electronic-warfare campaign" text).** Operator: "We can just assign a reflectivity and cross section to each chunk just like we do with entities, then their visibility depends on how much light is shining on them, no new mechanism needed ... This is what I actually intended with my ruling about deferring detection."
branch at each target-shaped read. Blast radius **(read)**: the count has grown since the first read. At `b66ba524`: 28 `Target` reads in `ActionGameManager.cs`, 22 in
`FireControl.cs`, 11 in `Entity.cs`, plus Unity weapon-manager sites (`GuidedProjectileManager`, `LightningGunManager`,
`ConstantParticleWeaponManager`, `GuidedProjectile`) the first count omitted. The first read was about 60 in 12 files
**Q7 (blocks Cut 5). Where does composition live?** **Resolved 2026-09-30: A′** (a per-kind entry list of commodity, abundance and affinity, derived, never stored per chunk), with Q11 (field kinds are authored catalog records yielding simple commodities). The per-ore `Abundance` on `SimpleCommodityData` recommended below is superseded: it gives one table for every field kind, which the Q2 ruling (tables are per field kind) contradicts.
**Q9 (blocks Cut 6). Which AI ships mine?** **Ruled 2026-09-30: C** (defer; against the recommendation below). Cut 6 is deleted. Nothing assigns a `Mining` task today. `Zone.CreateAgent`
(`Zone.cs:122-129`) gives every NPC a `PatrolOrbitsTask`.
- A: In a zone with belts, an NPC whose loadout has a weapon with nonzero affinity to some belt ore gets a `Mining`
task on the nearest belt, with an authored share (`GameplaySettings.AgentMinerShare`).
- B: Only dedicated miner hulls or factions mine. This needs faction-territory roles (`docs/faction-territory-target.md:36-49`,
"extraction").
- C: Defer AI mining. The same path is proven by a test agent only.
- **Recommended: A** for this campaign. B is the faction-territory campaign's call and can replace A's
eligibility rule without touching the path.
---
# Part II: Body map
## The belt simulation today
**(read)** `Zone` owns belts (`Zone.cs:26`, built at `:91-92`). Each tick, `Zone.Update` (`:149-179`):
1. advances `_time` (`:151`) and clears `_updatedOrbits` (`:152`);
2. waits on last tick's belt tasks (`:153-155`);
3. steps every orbit (`:156-161`);
4. for each belt, copies `NewTransforms` into `Transforms`, copies `NewOrbitPosition` into `OrbitPosition`, and
starts one `Task.Run(UpdateAsteroidTransforms)` (`:163-168`).
`UpdateAsteroidTransforms` (`:249-275`) evaluates every asteroid into a `float4` (x, y, rotation, damage-scaled
size). The per-asteroid gravity sample is commented out (`:272`). The shader applies height instead: `Asteroid.shader:28-32`
samples `_NebulaSurfaceHeight` per vertex and adds `_AsteroidVerticalOffset`, and the renderer sets that offset
globally (`ZoneRenderer.cs:442`).
**Findings (probe), on the densest zone of the operator's save** (EAC-2265, radius 5418, 8 belts of 123, 29, 101, 132,
269, 99, 30 and 26 asteroids, 809 in total, 56 bodies; that save has generated 1 of its 64 zones):
- **Cost before:** shipped `Zone.Update` with entities stripped costs **400-1760 us wall per tick** (the main
thread blocks in `t.Wait()` at `:153-154`) and **125-420 us process CPU per tick**, over 5 clean runs.
- **Cost after** (the same zone with no belt work): **31-39 us wall, 26-37 us CPU per tick.** Belts become free
per tick.
- The belt arithmetic alone, run sequentially, is 76-94 us/tick for all 809 asteroids. The threading bought no
speed: task scheduling and the blocking wait cost more than the work.
- **One asteroid on demand costs 186-224 ns** (`OrbitPeriod.Evaluate`, `OrbitData.Evaluate` and the parent orbit
lookup). A targeting query over the largest belt (269 asteroids) is about 55 us, paid when asked and not per tick.
- **The threading is a data race.** **4 of 10 probe runs died** with `InvalidOperationException: Operations that
change non-concurrent collections must have exclusive access`, thrown from `Zone.GetOrbitPosition`
(`:209`/`:216`) inside a belt task. Parallel belt tasks, and the main thread's `_updatedOrbits.Clear()` at `:152`
(which runs before the wait at `:153`), mutate the same `HashSet` and `Dictionary` unsynchronized. .NET detects
it; Unity's Mono may corrupt silently instead. Deleting the threading is a correctness fix as well as a cost fix.
- **Every pose the sim or renderer reads is exactly one tick stale.** After `Update`, `Transforms` matches the pose
at the previous tick's time with 0.0000 error, and misses the current pose by 0.209 units at the measured maximum
speed of 12.6 u/s. That comes from the double buffer (`:165`).
- **The operator's history, confirmed.** `GetHeight` for every asteroid, the commented-out line `:272`, costs
**1.9-2.1 ms/tick** over 56 bodies. That really was the heaviest per-tick operation. It is already gone from the
sim, and the shader now does the height.
## Consumers of the belt state
| Surface | Readers (HEAD) | Fate |
|---|---|---|
| `AsteroidBelt.Transforms` / `NewTransforms` (`Zone.cs:505-506`) | `Zone.cs:165,230,273`; `MiningTool.cs:62`; `ResourceScanner.cs:80`; `ZoneRenderer.cs:456,462,467,476`; `AsteroidBeltUI.Update` (`ZoneRenderer.cs:660-676`) | Deleted (Cut 1). The renderer fills its own buffers from the one function. |
| `AsteroidBelt.OrbitPosition` / `NewOrbitPosition` (`:508-509`) | `Zone.cs:166,256,271`; `ZoneRenderer.cs:449,451,662` | Deleted. The belt centre is `Zone.GetOrbitPosition(parent)`. `AsteroidBeltUI` already stores `_orbitParent` (`:593,613`) and never reads it. |
| `NearestAsteroid` (`:226-245`) | none. It reads `Transforms[i].xz` (x and rotation) where it means `.xy` | Deleted. Cut 3 adds the one chunk query. |
| `AsteroidExists` (`:247`) | `MiningTool.cs:61` | Replaced by `Zone.ChunkExists` in Cut 2. |
| `RespawnTimers`, `Damage`, `MiningAccumulator` (`:510-512`) | `Zone.cs:260-264,306-318` | Replaced by the wear store (Cut 2). |
| `Zone._random` (`:33,71`) | `MineAsteroid` only (`:323-324`) | Deleted with `MineAsteroid`, Q1=A. It is a shared stream, which Cut 6b forbids for combat dice. |
| `ActionGameManager` save/continue | no belt reader. `RunSave.Capture` packs entities, orbits and planet refs only | Gains wear through `Zone.PackZone` (Cut 2). |
**History, anchored at `8bd25f6f`.** Every line number below is stale (`FireControl.cs` is 1,614 lines at `b66ba524`, `Splash` is gone, and the chunk visibility and info rows are superseded by Cut 3). The re-anchored, live version is in Cuts 3 and 4.
| `source.VisibleEntities.Contains` | `PFire``:173`, `Inspect``:248` | within chunk detection range (Q2) |
| `EntityInfoGathered[target]` → `PSensor` | `PFire``:179-180` | info 1 |
| `LockWeapon.IsLocked` | `PFire``:175` | never locked (Q2) |
| `InArc(toTarget)` | `PFire``:176` | unchanged |
| `target.Velocity` | `PredictedIntercept``:135-138`, `Fire``:349` | `Zone.ChunkVelocity`: analytic orbital velocity plus the parent `Orbit.Velocity` (`Zone.cs:160`) |
| `target.Hull`, `Silhouette(target, hull, aimed, bearing, precision)` | `Forecast``:196-205`, `CommitProbability``:486-487`, `Commit``:531` | a disc of radius `r / SchematicCellSize` cells, one interval `[-R, R)`, `a = 0`, bearing-free |
| `DeviationProbability` (live position) | `:310-316` | unchanged in shape. A chunk's curvature over a 2 s flight is `v²t²/2R` ≈ 0.16 u at the probe belt, well inside any Tracking |
Compounds: Methane, Water. Organics: Algae, Animal Products, Bacteria, Fungi. Ammo: Ammo. Every one is priced 0 except Ammo (1000). The catalog oid moved after `8bd25f6f` (`4f218e85`, `dd437580`, locomotion Cut 1): `1862ddc4` then, `2aa79c0a` at `b66ba524`.
`Maximum`, `Exponent`, `Floor`; `17673cd2`) were keys 6, 7, 8 and 11 on `SimpleCommodityData`. Only key 11 stays retired there; 6, 7 and 8 are live `ItemData` keys (`SpecificHeat`, `Conductivity`, `Price`). No values remain in the deleted
Status: cut map. Ends are owned by Part I of this document; Part III owns the means. Updated at M0, 2026-09-30.
**M0 (2026-09-30):** `codex/mining` merged with master (fire control, `dcd7bbc5`) and this map corrected against
`docs/mining-cut-refresh.md` (v2). Cuts 3 and 4 below are the refresh's versions. The refresh's Cut 5 and Cut 7 texts are
the current proposals and are not yet folded into this file; where they differ, the refresh wins.
Rulings (operator), 2026-09-25: Q1 = A, Q2 = A (one slot, two kinds; field generalization), Q3 = A.
Rulings (operator), 2026-09-30:
| Q | Ruling |
|---|---|
| Q4 | A, the formula as written (Cut 5) |
| Q5 | A, per hit (Cut 5) |
| Q6a | A, straight into cargo; overflow lost (Cut 5) |
| Q6b | A, one lot per (zone, field, commodity) source (Cut 5) |
| Q7 | A′, a per-kind entry list (commodity, abundance, affinity), derived, never stored per chunk (Cuts 3 and 5) |
| Q8 | **B**, author Corrosive and Ionizing weapons (Cut 7), against the earlier recommendation |
| Q9 | **C**, AI mining deferred; the path is proven by a test agent in Cut 5. Cut 6 is deleted |
| Q10 | A, a blast covers every chunk it overlaps; the shooter gets the loot (Cuts 4 and 5) |
| Q11 | field kinds are authored catalog records; tables yield simple commodities only (Cuts 3, 5, 7) |
| Q12 | A, launchers cannot mine (Cuts 3 and 4) |
| Q13 | A, reach is the longest active weapon range |
| Q2 correction | Chunks are detected by the existing reflectance rule (reflectivity and cross-section, lit by the suns), not deferred to EW. Struck: the scanner as chunk sensor, the fallback range tunable, unconditional visibility, PSensor 1. `ResourceScanner` stays parked |
**Cut 2: landed 2026-09-25** at `3b9f0f1d`, `c924bae0`, `daa64560`. 272 tests; production net −73 lines; `AetherDb census` byte-identical; break threshold kept strictly `>`; `ZonePack` key 6 nullable, proven by a raw-MessagePack older-record test. Spec discrepancy fixed: the retirement comment for union tag 26 no longer names `MiningToolData`, which the cut's own negative grep forbids. Soul found one real defect, dormant until Cut 4 wires `FireControl.Apply` into `Wear`: a hit on a chunk that was
already broken reset `BrokenUntil` and resurrected it on the spot (any shot resolving after the chunk broke —
travel time, two shots close together). Fixed at `34994732`: a hit on a broken chunk changes nothing and `Wear`
returns false; pinned below and above hitpoints and across a save taken inside the respawn window (both tests fail
on the old code). `93bfdebd` removes the dead `MiningDifficulty: 500` still serialized in `Settings.asset`. Soul also
confirmed the strict `>` threshold cannot stall (wear accumulates across hits; only zero damage never breaks).
274 tests. **Closed**; the two-commit fix's own check folds into Cut 3's Soul pass.
`FieldDriver.cs`, `ActionBarSlot.cs`: `.Entity` and null checks.
#### Authority map
- **Owners:**
-`Entity.Target` is the one slot, and `SetTarget` is its one writer.
-`Sensor.Gain` is the one detection gain rule.
-`FieldKindData` owns a chunk's reflectivity.
-`FieldKinds.Assign` owns which kind a belt is.
- **Inputs:** player handlers and the test agent; sensor stats; chunk pose and radius; sun light.
- **Outputs:** the current `TargetRef`; `ChunkVisibility`; `ChunkInfo`.
- **Derived state:**
-`ChunkVisibility` and `ChunkInfo` are pure functions of time and are never stored.
-`TargetItem` is entity-only.
-`TargetRange`.
- **Forbidden writers:**
- any second target field;
- any direct `Target.Value =` outside `SetTarget`;
- any chunk in `Zone.Entities` or `EntityInfoGathered`;
- any per-tick chunk detection loop;
- any chunk-specific copy of the gain arithmetic;
- any unconditional visibility for chunks.
- **Shared paths:** reticle, cycling, the test agent, dock and undock re-activation, and belt generation and load
(through `FieldKinds.Assign`).
- **Deletion line:** no `ReactiveProperty<Entity> Target` remains; no `Target.Value =` outside `SetTarget`; no
gain arithmetic outside `Sensor.Gain`.
#### Verification
Yggdrasil, the M0 command with `--filter FullyQualifiedName~MiningCut3`, then the full suite.
| Test | Rule it pins |
|---|---|
| `SensorGainExtractionChangesNothing` | The existing detection tests, and a recorded `EntityInfoGathered` trace over N ticks for a fixed scene, are byte-equal before and after the extraction. |
| `ChunkInfoMatchesAnEntityHeldStill` | Q14's parity. An entity with a `Reflector` of the same cross-section, at the chunk's position and held still, converges under the per-tick loop to the value `ChunkInfo` returns (tolerance from the discrete fixed point). **One rule, two integrators.** |
| `AChunkInDarknessIsNotVisible` | A chunk outside every sun's `LightRadius` has `ChunkVisibility` 0 and cannot be targeted or fired on. |
| `ABrighterOrBiggerChunkIsSeenFurther` | Monotone in cross-section, radius and light. |
- For a chunk, the visibility gate (`:173`) is `ChunkInfo > TargetDetectionInfoThreshold`, and `info` (`:179`)
is `ChunkInfo` (Cut 3). **This replaces v1's "no gate, info 1".**
-`PSensor` prices a chunk exactly as it prices a ship at the same info.
- A `LockWeapon` is gated out (`:175`, Q12 = A). The arc stays (`:176`).
-`Forecast``:206`, `HitProbability``:228`, `Inspect``:243-283`: the disc silhouette. `Inspect`'s `Visible` and
`Info` come from the chunk path.
-`PredictedIntercept``:133`, `TravelDirection``:307`: position and velocity through the ref. `Bearing``:316`
is not called for a chunk, because a disc has no facing.
-`DeviationProbability``:336-341`: the live chunk position is `Zone.ChunkPose` at `now`.
-`Fire``:355-418`: freezes the ref.
-`Step``:439`: `targetGone` includes `!ChunkExists`.
-`CommitProbability``:506-520`: for a chunk, the disc (no `GetData(Hull)` at `:516`).
-`Commit``:550-600`: for a chunk hit, no `Lane` (`:563-578`) and no shield block (`:585-592`). `Cell`, `Bearing`
and `Lateral` stay zero in `MakeOutcome` (`:1028`).
-`Apply``:651-678`:
- Null fuse on a chunk: `Zone.Wear(chunk, Damage)` (`Zone.cs:260` @`283ce7dc`).
- Contact or delayed fuse on a chunk: no `ApplyBlastHit`, which needs a hull (`:747-760`). `Detonate` at the
chunk's centre at arrival. The pose is a pure function of time, so the point is exact.
- Proximity: `Detonate` as today, now with the chunk pass.
- Contact or delayed on a ship: `ApplyBlastHit` as today, and its `Detonate` now also covers nearby chunks.
- Unity presentation: `EntityInstance.cs` (4 target reads) and the four weapon managers aim at the chunk's
rendered position. Presentation only.
**Authority map:**
- **Owner:** `FireControl`, unchanged. `Detonate` stays the one area owner.
- **New inputs:** chunk pose, velocity, radius and info, through `Zone` and `Entity.ChunkInfo`.
- **When blast damage to chunks is decided:** at detonation. Each covered chunk is judged where it is at arrival,
which is 12.4's own bystander rule (`ApplyBlastHit`'s comment at `:773-777`: "every bystander is judged against
where it actually is at arrival"). A direct hit on a chunk is still decided at `Commit`.
- **Forbidden writers:**
- any chunk hit test outside `Commit`, except `Detonate`'s area pass;
- any second area-share formula;
- any second `Phi` integrator;
-`Detonate` reading a host.
- **Shared paths:** player trigger, the Cut 5 test agent, AI `Activate` (`Combat.cs:121`), and turrets (which
never hold a chunk). Any blast from any source goes through `Detonate`.
- **Deletion line:** none.
**Verification** (Yggdrasil, `--filter FullyQualifiedName~MiningCut4`, then the full suite):
| Test | Rule it pins |
|---|---|
| `AChunkShotUsesTheSameFactors` | `Accuracy`, `PSpread`, `Sigma` and `PSensor` equal a ship shot's at equal range, precision and info. One fire path. |
| `AnUnseenChunkCannotBeFiredOn` | The gate is the detection rule, not a constant. |
| `HullSilhouetteUnchangedByTheExtraction` | Every existing `Silhouette` test and the `ProbeC` equivalence still pass. The negative for the extraction. |
| `ChunkHitsWearTheChunk`, `AHitOnABrokenChunkChangesNothing` | Wear; re-pins `34994732` through the real path. |
| `ABlastConservesAcrossCellsAndChunks` | The shares delivered to cells plus chunks, plus the uncovered area, equal the damage. Same lattice style as 12.4's 1,944-disc conservation probe. |
| `ABlastBesideAnUntargetedRockWearsIt` | Q10 = A. |
| `CircleCircleOverlapIsTheLens` | Closed form, with the tangent, contained and disjoint cases (12.4's F1 tangent lesson). |
| `LaunchersCannotFireOnAChunk` | Q12 = A. |
| `ChunkDiceAreTheShotsOwn` | The same galaxy and shot id give the same outcome. |
Also: Stryker over the new branches. Operator: shoot a lit rock with each damage type; it shrinks and breaks,
and misses at range look plausible.
**Subtraction estimate:** about 5 removed (the integrator extraction) and about 115 added (the chunk pass and the