2025-07-18 00:21:33 +01:00
|
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
2020-12-22 03:13:43 +00:00
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
|
|
|
|
using System;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Linq;
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
using GameCult.Caching;
|
2020-12-01 00:16:11 +00:00
|
|
|
|
using UniRx;
|
2026-09-14 17:39:12 +02:00
|
|
|
|
using CultMath;
|
|
|
|
|
|
using static CultMath.math;
|
|
|
|
|
|
using float2 = CultMath.float2;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
|
|
|
|
|
public class Zone
|
|
|
|
|
|
{
|
2021-02-11 03:14:20 +00:00
|
|
|
|
public Action<string> Log;
|
2026-10-13 02:13:43 +02:00
|
|
|
|
|
|
|
|
|
|
// Membership, in admission order. Admit and Remove are the only writers; everyone else reads this view, which has
|
|
|
|
|
|
// no mutating member, so the death watch below follows membership by construction.
|
|
|
|
|
|
private readonly ReactiveCollection<Entity> _entities = new ReactiveCollection<Entity>();
|
|
|
|
|
|
public MemberView Entities { get; }
|
|
|
|
|
|
|
|
|
|
|
|
public sealed class MemberView : IReadOnlyReactiveCollection<Entity>
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly ReactiveCollection<Entity> _members;
|
|
|
|
|
|
internal MemberView(ReactiveCollection<Entity> members) => _members = members;
|
|
|
|
|
|
public int Count => _members.Count;
|
|
|
|
|
|
public Entity this[int index] => _members[index];
|
|
|
|
|
|
public int IndexOf(Entity entity) => _members.IndexOf(entity);
|
|
|
|
|
|
public bool Contains(Entity entity) => _members.Contains(entity);
|
|
|
|
|
|
public IEnumerator<Entity> GetEnumerator() => _members.GetEnumerator();
|
|
|
|
|
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
|
|
|
|
|
public UniRx.IObservable<CollectionAddEvent<Entity>> ObserveAdd() => _members.ObserveAdd();
|
|
|
|
|
|
public UniRx.IObservable<int> ObserveCountChanged(bool notifyCurrentCount = false) => _members.ObserveCountChanged(notifyCurrentCount);
|
|
|
|
|
|
public UniRx.IObservable<CollectionMoveEvent<Entity>> ObserveMove() => _members.ObserveMove();
|
|
|
|
|
|
public UniRx.IObservable<CollectionRemoveEvent<Entity>> ObserveRemove() => _members.ObserveRemove();
|
|
|
|
|
|
public UniRx.IObservable<CollectionReplaceEvent<Entity>> ObserveReplace() => _members.ObserveReplace();
|
|
|
|
|
|
public UniRx.IObservable<Unit> ObserveReset() => _members.ObserveReset();
|
|
|
|
|
|
}
|
2026-10-07 19:47:21 +02:00
|
|
|
|
|
|
|
|
|
|
// Laid mines, in lay order. Zone owns their existence: Lay is the one adder, the mine step the one remover,
|
|
|
|
|
|
// and every removal is a blast. Runtime state, never saved.
|
|
|
|
|
|
public ReactiveCollection<Mine> Mines = new ReactiveCollection<Mine>();
|
2026-10-09 20:22:00 +02:00
|
|
|
|
|
|
|
|
|
|
// Vented vapour clouds, in vent order. Zone owns their existence: Vent is the one adder, StepClouds the one
|
|
|
|
|
|
// remover, and Obscuration the one reader that detection uses. Runtime state, never saved.
|
|
|
|
|
|
public ReactiveCollection<VapourCloud> Clouds = new ReactiveCollection<VapourCloud>();
|
2026-10-12 04:00:08 +02:00
|
|
|
|
|
|
|
|
|
|
// Floating items (death drops, jettisoned cargo), in release order. Zone owns their existence: Release is the one
|
2026-10-13 05:00:22 +02:00
|
|
|
|
// adder; StepFloatingItems (expiry) and TryPickUp are the only removers. TryHold, Free and Remove-of-the-holder
|
|
|
|
|
|
// are the only writers of FloatingItem.Holder. Runtime state, never saved.
|
2026-10-12 04:00:08 +02:00
|
|
|
|
public ReactiveCollection<FloatingItem> FloatingItems = new ReactiveCollection<FloatingItem>();
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
public Dictionary<CultRecordKey, BodyData> Planets = new Dictionary<CultRecordKey, BodyData>();
|
|
|
|
|
|
public Dictionary<CultRecordKey, Planet> PlanetInstances = new Dictionary<CultRecordKey, Planet>();
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
public Dictionary<CultRecordKey, Orbit> Orbits = new Dictionary<CultRecordKey, Orbit>();
|
|
|
|
|
|
public Dictionary<CultRecordKey, AsteroidBelt> AsteroidBelts = new Dictionary<CultRecordKey, AsteroidBelt>();
|
2021-02-11 03:14:20 +00:00
|
|
|
|
public PlanetSettings Settings;
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
|
2026-10-03 15:02:51 +02:00
|
|
|
|
// Mining index (docs/aetheria-release-map.md, L3-L5): every targetable thing in the zone, fed by providers --
|
|
|
|
|
|
// the entities, and one provider per belt. The reticle, cycling and any range query over rocks go through it;
|
|
|
|
|
|
// nothing else scans a belt.
|
|
|
|
|
|
public readonly TargetingIndex Targets = new TargetingIndex();
|
|
|
|
|
|
private readonly List<Sun> _suns = new List<Sun>();
|
|
|
|
|
|
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
private HashSet<CultRecordKey> _updatedOrbits = new HashSet<CultRecordKey>();
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
2021-03-13 03:23:58 +00:00
|
|
|
|
private ItemManager _itemManager;
|
2021-03-27 10:03:24 +00:00
|
|
|
|
private double _time;
|
2021-03-09 02:26:58 +00:00
|
|
|
|
public List<Agent> Agents = new List<Agent>();
|
2026-10-22 15:39:02 +02:00
|
|
|
|
// One flight per side, created on its first piloted join. A side is a faction, or one unaffiliated ship
|
|
|
|
|
|
// (brain-one-per-side-r2). A flight owns what its members target and do (Agents/Flight.cs).
|
|
|
|
|
|
private readonly Dictionary<object, Flight> _flights = new Dictionary<object, Flight>();
|
2026-10-08 14:18:41 +02:00
|
|
|
|
public IEnumerable<Flight> Flights => _flights.Values;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
// Cut 2 (docs/mining-cut.md): chunk wear, keyed by chunk. Zone.Wear is the only writer; ChunkExists and
|
|
|
|
|
|
// ChunkRadius are the only readers besides the pack/unpack round trip below.
|
|
|
|
|
|
private Dictionary<ChunkId, ChunkWear> _wear = new Dictionary<ChunkId, ChunkWear>();
|
|
|
|
|
|
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
|
// Cut 3 (docs/fire-control-cut.md, 0b table): Zone owns the pending-shot collection and the ShotId
|
|
|
|
|
|
// namespace; FireControl owns every transition a shot goes through. ShotCommitted publishes once a shot's
|
|
|
|
|
|
// outcome is decided (R4's commit horizon); ShotResolved republishes the same, unchanged outcome at
|
|
|
|
|
|
// arrival, right before the shot is removed. Presentations are the only readers of either.
|
|
|
|
|
|
public List<PendingShot> PendingShots = new List<PendingShot>();
|
|
|
|
|
|
public Subject<ShotOutcome> ShotCommitted = new Subject<ShotOutcome>();
|
|
|
|
|
|
public Subject<ShotOutcome> ShotResolved = new Subject<ShotOutcome>();
|
2026-10-12 05:46:32 +02:00
|
|
|
|
// Contacts.Step publishes each hull-on-hull contact it resolves; presentations are the only readers.
|
|
|
|
|
|
public Subject<ContactEvent> Contacted = new Subject<ContactEvent>();
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
|
private int _nextShotId;
|
|
|
|
|
|
public int NextShotId() => ++_nextShotId;
|
|
|
|
|
|
|
2026-09-29 16:27:54 +02:00
|
|
|
|
// The committed shot a presentation was handed the id of by OnFire. It carries what the simulation decided
|
|
|
|
|
|
// -- the target it flies at (null for a round that carries none) and where it bursts -- so a presentation
|
|
|
|
|
|
// reads that rather than the shooter's current selection.
|
|
|
|
|
|
public bool TryGetShot(int shotId, out PendingShot shot)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var pending in PendingShots)
|
|
|
|
|
|
if (pending.ShotId == shotId)
|
|
|
|
|
|
{
|
|
|
|
|
|
shot = pending;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
shot = default;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-11 22:03:20 +02:00
|
|
|
|
private readonly List<Departure> _departures = new List<Departure>();
|
|
|
|
|
|
internal IReadOnlyList<Departure> Departures => _departures;
|
|
|
|
|
|
|
2021-03-27 10:03:24 +00:00
|
|
|
|
public float Time
|
|
|
|
|
|
{
|
|
|
|
|
|
get => (float) _time;
|
|
|
|
|
|
}
|
2026-10-03 15:02:51 +02:00
|
|
|
|
|
|
|
|
|
|
// Zone time at full precision, for the index's orbital keys, which must agree with ChunkPose's own.
|
Flow: register the flow keywords, one zone-time value for Zone.Flow and the scroll, presenter phases from zone time, folded scroll, tighter parity
- VolumeSampling registers FLOW_GLOBAL / FLOW_SLOPE with GlobalKeyword.Create (the constructor only looks up).
- FlowField.Scroll folds each Tri3D octave's phase modulo 1 in double (float2, [0,1)); Zone.Flow evaluates at Zone.FlowScroll.
- FlowField.Cycle: the nebula's flow cycle and Stardust's particle lifetime phase come from zone time (_FlowCycle, phase uniform), not _Time.y / Time.time.
- FlowFieldParityVerify reaches the keyword-gated flow(), fails on logged errors, tolerance 3e-4; mutants updated.
Not built or run when committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:43:41 +02:00
|
|
|
|
public double ExactTime => _time;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public ZonePack Pack { get; }
|
2021-05-14 03:03:28 +01:00
|
|
|
|
public GalaxyZone GalaxyZone { get; }
|
|
|
|
|
|
public Galaxy Galaxy { get; }
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
2026-09-19 18:34:04 +02:00
|
|
|
|
// Cut 6b (docs/fire-control-cut.md, 6.1, Soul finding 6): one zone, one stable identity -- a galaxy seed
|
|
|
|
|
|
// reproduces it, and nothing else in the zone (an NPC joining, a shop opening, a wormhole exit) can
|
|
|
|
|
|
// perturb it. FireControl.Commit builds its own local generator from this and the shot's own id instead
|
|
|
|
|
|
// of touching a shared stream.
|
|
|
|
|
|
public uint CombatSeed { get; }
|
|
|
|
|
|
|
2026-10-18 17:30:50 +02:00
|
|
|
|
// The zone's drop dice (loot and mine scatter): CombatSeed mixed with its run's seed (ruling loot-seed-in-save), so a
|
|
|
|
|
|
// new game drops differently and a reloaded save repeats. Mix(0) is 0, so a zone with no galaxy rolls on CombatSeed.
|
|
|
|
|
|
public uint DropSeed { get; }
|
|
|
|
|
|
|
2026-10-07 14:52:12 +02:00
|
|
|
|
// The one source of floating item and mine ids: zone-scoped, from 1, never reused. Runtime state, not saved.
|
|
|
|
|
|
private uint _lastBodyId;
|
|
|
|
|
|
public FloatingBodyId NextBodyId() => new FloatingBodyId(++_lastBodyId);
|
|
|
|
|
|
|
2026-10-18 17:30:50 +02:00
|
|
|
|
// The death ordinal of the loot dice: the nth death in this zone rolls on SimulationDice.For(DropSeed, LootStream, n).
|
2026-10-12 04:00:08 +02:00
|
|
|
|
private uint _deaths;
|
|
|
|
|
|
|
2026-10-17 20:46:37 +02:00
|
|
|
|
// The zone-scoped agent ordinal, from 1, never reused: the key of a pilot's own dice.
|
|
|
|
|
|
private uint _lastAgentId;
|
|
|
|
|
|
public uint NextAgentId() => ++_lastAgentId;
|
|
|
|
|
|
|
2026-10-13 02:13:43 +02:00
|
|
|
|
// One death subscription per member entity: Admit creates it together with the membership, Remove disposes it
|
|
|
|
|
|
// together with the membership, and nothing else touches either.
|
2026-10-12 20:49:42 +02:00
|
|
|
|
private readonly Dictionary<Entity, IDisposable> _deathWatches = new Dictionary<Entity, IDisposable>();
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public Zone(ItemManager itemManager, PlanetSettings settings, ZonePack pack, GalaxyZone galaxyZone, Galaxy galaxy)
|
2020-11-27 22:47:33 +00:00
|
|
|
|
{
|
2026-10-13 02:13:43 +02:00
|
|
|
|
Entities = new MemberView(_entities);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
_time = pack.Time;
|
2021-05-14 03:03:28 +01:00
|
|
|
|
GalaxyZone = galaxyZone;
|
|
|
|
|
|
Galaxy = galaxy;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Pack = pack;
|
2021-03-13 03:23:58 +00:00
|
|
|
|
_itemManager = itemManager;
|
2021-01-06 14:56:39 +00:00
|
|
|
|
Settings = settings;
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
_orbitPositionAt = OrbitPositionAt;
|
2026-09-19 18:34:04 +02:00
|
|
|
|
CombatSeed = galaxyZone?.Name.StableHash() ?? 1337u;
|
2026-10-18 17:30:50 +02:00
|
|
|
|
DropSeed = CombatSeed ^ SimulationDice.Mix(galaxy?.Seed ?? 0u);
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
var cache = itemManager.ItemData;
|
|
|
|
|
|
|
Fire control Cut 5: close the rules that landed on one path and not its twin
Soul's pass over the whole campaign confirmed eleven findings. Seven were code
defects of one shape -- a rule implemented on one path and left off the path
beside it -- and this is all seven.
5.1 Tracking falls back to GameplaySettings.UnaidedTracking (new, 10) instead of
zero, and Commit drops the zero branch that turned a Tracking-less shooter
into a hard wall. Unaided fire was meant to be really, really bad; it was
334 shots and no hits against a target drifting at 5 u/s.
5.2 A shot the shield cannot absorb now breaks it. The decision is frozen into
ShotOutcome.ShieldBroken at commit and performed in Apply, so a shield that
recharges mid-flight cannot retroactively survive. Splash already did this;
the discrete roll never did, which made the whole shield-break mechanic
unreachable for every weapon in the catalog.
5.3 ConstantWeapon gates on ArcAllowsFire alongside StanceAllowsFire. A
side-mounted beam was firing forward.
5.4 The point-blank bearing case moves out of Weapon.ArcAllowsFire and into
InArc, which used to normalize the zero vector into NaN. The trigger said
fire and the probability said impossible.
5.5 A destroyed item drops out of IsRevealed's ranking, so Precision stops
concentrating hits onto a dead subsystem's footprint.
5.6 Zone subscribes Entity.Death and removes the corpse itself. That removal
lived only in Unity's loot-drop subscription, so headless nothing ever left
the zone, shots landed on corpses and no fight could end.
5.7 PendingShot drops PredictedIntercept and FlightTime, written and read
nowhere.
208 tests pass (197 plus eleven new).
Known defect, recorded rather than hidden: the committed mutation harness is
unreliable. Across two runs it failed to restore FireControl.cs and Weapon.cs,
leaving live mutations in the working tree -- including its own 5.1 mutation,
which is how a green suite briefly reported two failures. Its verdicts are
therefore evidence only where re-run from a verified-clean tree. Two anchors
(Zone.cs's ObserveAdd line, Weapon.cs's InArc return) fail to match at all, and
5.3's ConstantWeaponObeysArc survives its own mutation, so that rule is written
but not pinned. Cut 5b owns the repair; nothing here is trusted on the harness's
word, and the suite result above was taken by hand from a hash-verified tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:56:40 +02:00
|
|
|
|
// Cut 5, 5.6 (docs/fire-control-cut.md, Soul finding 7; operator ruling Q3): death removes the ship,
|
|
|
|
|
|
// in the simulation, not only in Unity's own loot-drop subscription. One subscription point covers
|
2026-09-30 12:32:59 +02:00
|
|
|
|
// every join, whichever caller admits the entity (deserialization below, a jump, a spawned turret) --
|
Fire control Cut 5: close the rules that landed on one path and not its twin
Soul's pass over the whole campaign confirmed eleven findings. Seven were code
defects of one shape -- a rule implemented on one path and left off the path
beside it -- and this is all seven.
5.1 Tracking falls back to GameplaySettings.UnaidedTracking (new, 10) instead of
zero, and Commit drops the zero branch that turned a Tracking-less shooter
into a hard wall. Unaided fire was meant to be really, really bad; it was
334 shots and no hits against a target drifting at 5 u/s.
5.2 A shot the shield cannot absorb now breaks it. The decision is frozen into
ShotOutcome.ShieldBroken at commit and performed in Apply, so a shield that
recharges mid-flight cannot retroactively survive. Splash already did this;
the discrete roll never did, which made the whole shield-break mechanic
unreachable for every weapon in the catalog.
5.3 ConstantWeapon gates on ArcAllowsFire alongside StanceAllowsFire. A
side-mounted beam was firing forward.
5.4 The point-blank bearing case moves out of Weapon.ArcAllowsFire and into
InArc, which used to normalize the zero vector into NaN. The trigger said
fire and the probability said impossible.
5.5 A destroyed item drops out of IsRevealed's ranking, so Precision stops
concentrating hits onto a dead subsystem's footprint.
5.6 Zone subscribes Entity.Death and removes the corpse itself. That removal
lived only in Unity's loot-drop subscription, so headless nothing ever left
the zone, shots landed on corpses and no fight could end.
5.7 PendingShot drops PredictedIntercept and FlightTime, written and read
nowhere.
208 tests pass (197 plus eleven new).
Known defect, recorded rather than hidden: the committed mutation harness is
unreliable. Across two runs it failed to restore FireControl.cs and Weapon.cs,
leaving live mutations in the working tree -- including its own 5.1 mutation,
which is how a green suite briefly reported two failures. Its verdicts are
therefore evidence only where re-run from a verified-clean tree. Two anchors
(Zone.cs's ObserveAdd line, Weapon.cs's InArc return) fail to match at all, and
5.3's ConstantWeaponObeysArc survives its own mutation, so that rule is written
but not pinned. Cut 5b owns the repair; nothing here is trusted on the harness's
word, and the suite result above was taken by hand from a hash-verified tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:56:40 +02:00
|
|
|
|
// ObserveAdd fires for all of them. Forbidden writer: no presentation may remove an entity from Zone.
|
2026-10-12 20:49:42 +02:00
|
|
|
|
// The death watch lives exactly as long as the entity's membership in this zone: installed on admission,
|
|
|
|
|
|
// disposed on removal (death, dock, jump), so an entity admitted again never holds a second one.
|
2026-10-11 22:03:20 +02:00
|
|
|
|
// The one record of a removal for observers (ruling reveal-ends-on-perception): read by the next Sense pass.
|
2026-10-13 02:13:43 +02:00
|
|
|
|
_entities.ObserveRemove().Subscribe(remove => _departures.Add(new Departure(remove.Value, remove.Value.Position.xz,
|
2026-10-12 20:51:46 +02:00
|
|
|
|
remove.Value.Visibility)));
|
Fire control Cut 5: close the rules that landed on one path and not its twin
Soul's pass over the whole campaign confirmed eleven findings. Seven were code
defects of one shape -- a rule implemented on one path and left off the path
beside it -- and this is all seven.
5.1 Tracking falls back to GameplaySettings.UnaidedTracking (new, 10) instead of
zero, and Commit drops the zero branch that turned a Tracking-less shooter
into a hard wall. Unaided fire was meant to be really, really bad; it was
334 shots and no hits against a target drifting at 5 u/s.
5.2 A shot the shield cannot absorb now breaks it. The decision is frozen into
ShotOutcome.ShieldBroken at commit and performed in Apply, so a shield that
recharges mid-flight cannot retroactively survive. Splash already did this;
the discrete roll never did, which made the whole shield-break mechanic
unreachable for every weapon in the catalog.
5.3 ConstantWeapon gates on ArcAllowsFire alongside StanceAllowsFire. A
side-mounted beam was firing forward.
5.4 The point-blank bearing case moves out of Weapon.ArcAllowsFire and into
InArc, which used to normalize the zero vector into NaN. The trigger said
fire and the probability said impossible.
5.5 A destroyed item drops out of IsRevealed's ranking, so Precision stops
concentrating hits onto a dead subsystem's footprint.
5.6 Zone subscribes Entity.Death and removes the corpse itself. That removal
lived only in Unity's loot-drop subscription, so headless nothing ever left
the zone, shots landed on corpses and no fight could end.
5.7 PendingShot drops PredictedIntercept and FlightTime, written and read
nowhere.
208 tests pass (197 plus eleven new).
Known defect, recorded rather than hidden: the committed mutation harness is
unreliable. Across two runs it failed to restore FireControl.cs and Weapon.cs,
leaving live mutations in the working tree -- including its own 5.1 mutation,
which is how a green suite briefly reported two failures. Its verdicts are
therefore evidence only where re-run from a verified-clean tree. Two anchors
(Zone.cs's ObserveAdd line, Weapon.cs's InArc return) fail to match at all, and
5.3's ConstantWeaponObeysArc survives its own mutation, so that rule is written
but not pinned. Cut 5b owns the repair; nothing here is trusted on the harness's
word, and the suite result above was taken by hand from a hash-verified tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:56:40 +02:00
|
|
|
|
|
2026-10-08 14:18:41 +02:00
|
|
|
|
// Leaving Entities, by death, docking or a transfer, is leaving the zone's pilots: the agent and its flight
|
|
|
|
|
|
// membership go with the ship, so nothing keeps flying a hull that is not here.
|
|
|
|
|
|
Entities.ObserveRemove().Subscribe(remove =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var agent = Agents.FirstOrDefault(a => a.Ship == remove.Value);
|
|
|
|
|
|
if (agent == null) return;
|
|
|
|
|
|
Agents.Remove(agent);
|
|
|
|
|
|
foreach (var flight in _flights.Values) flight.Leave(agent);
|
2026-10-22 15:39:02 +02:00
|
|
|
|
// A side of one is gone with its ship.
|
|
|
|
|
|
if (remove.Value.Faction == null) _flights.Remove(remove.Value);
|
2026-10-08 14:18:41 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-03 15:02:51 +02:00
|
|
|
|
Targets.Add(new EntityTargets());
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
foreach (var orbit in pack.Orbits)
|
2020-11-27 22:47:33 +00:00
|
|
|
|
{
|
2026-10-15 18:11:27 +02:00
|
|
|
|
Orbits.Add(orbit.Key, new Orbit(cache.Get(orbit)));
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
|
|
|
|
|
|
foreach (var body in pack.Planets)
|
2020-12-01 00:16:11 +00:00
|
|
|
|
{
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
var planet = cache.Get(body);
|
|
|
|
|
|
Planets.Add(body.Key, planet);
|
2020-12-01 00:16:11 +00:00
|
|
|
|
switch (planet)
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
case AsteroidBeltData belt:
|
Mining Cut 3: field kinds, chunk visibility, the chunk query, and TargetRef
- FieldKindData, a catalog document (Q11): a kind's reflectivity per schematic
cell and its generation weight. AetheriaStores routes it to the catalog.
- AsteroidBeltData.Kind, key 10 (Q16 B). FieldKinds.Assign is the one pick (a
weighted choice by a stable hash of the belt's key); FieldKinds.Ensure gives
a kindless belt its kind and saves it. ZoneGenerator calls it at generation
and Zone calls it on first load, so an old belt is assigned once by the same
rule and keeps its kind after later catalog edits.
- Zone.ChunkVisibility: Reflector's rule for a chunk -- kind cross-section x
area in schematic cells x the light on it. Stored nowhere; a worn chunk dims,
a broken one or a kindless belt's is dark.
- Zone.ChunksNear: the one chunk query, skipping belts whose annulus cannot
reach. AsteroidBelt gains InnerRadius for that skip.
- TargetRef: an entity, a chunk, or none, with value equality and no ==.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:24 +02:00
|
|
|
|
// Mining Cut 3 (Q16 B): a belt from before field kinds existed gets its kind here, once, by
|
|
|
|
|
|
// the rule generation uses, and keeps it.
|
|
|
|
|
|
FieldKinds.Ensure(body.Key, belt, cache);
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
AsteroidBelts[body.Key] = new AsteroidBelt(belt);
|
2020-12-01 00:16:11 +00:00
|
|
|
|
break;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
case SunData sun:
|
2026-10-03 15:02:51 +02:00
|
|
|
|
var sunInstance = new Sun(settings, sun, Orbits[planet.Orbit.Key]);
|
|
|
|
|
|
PlanetInstances.Add(body.Key, sunInstance);
|
|
|
|
|
|
_suns.Add(sunInstance);
|
2021-04-12 04:20:52 +01:00
|
|
|
|
break;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
case GasGiantData gas:
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
PlanetInstances.Add(body.Key, new GasGiant(settings, gas, Orbits[planet.Orbit.Key]));
|
2020-12-01 00:16:11 +00:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
PlanetInstances.Add(body.Key, new Planet(settings, planet, Orbits[planet.Orbit.Key]));
|
2020-12-01 00:16:11 +00:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2021-03-13 03:23:58 +00:00
|
|
|
|
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
// Orbits, bodies and belts exist: the field's terms are built once, and every rail's period is derived from them.
|
|
|
|
|
|
var wellMass = new Dictionary<CultRecordKey, float>();
|
2026-10-15 18:11:27 +02:00
|
|
|
|
foreach (var body in PlanetInstances)
|
|
|
|
|
|
if (body.Value.WellStrength > 0)
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
wellMass.TryAdd(Planets[body.Key].Orbit.Key, body.Value.BodyData.Mass);
|
2026-10-16 21:14:58 +02:00
|
|
|
|
var beltExtent = new Dictionary<CultRecordKey, float>();
|
|
|
|
|
|
foreach (var belt in AsteroidBelts)
|
|
|
|
|
|
{
|
2026-10-18 16:28:42 +02:00
|
|
|
|
var rail = Planets[belt.Key].Orbit.Key;
|
2026-10-21 20:28:35 +02:00
|
|
|
|
beltExtent[rail] = AsteroidBelt.Extent(belt.Value.Data, Orbits[rail].Data.Distance);
|
2026-10-16 21:14:58 +02:00
|
|
|
|
}
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
var wellNodes = new List<WellNode>();
|
|
|
|
|
|
foreach (var orbit in Orbits)
|
2026-10-16 21:14:58 +02:00
|
|
|
|
{
|
|
|
|
|
|
var data = orbit.Value.Data;
|
|
|
|
|
|
wellNodes.Add(new WellNode(orbit.Key, data.Parent.Key, data.Distance, data.Phase,
|
|
|
|
|
|
wellMass.TryGetValue(orbit.Key, out var mass) ? mass : 0f,
|
2026-10-18 16:28:42 +02:00
|
|
|
|
beltExtent.TryGetValue(orbit.Key, out var reach) ? reach : data.Distance,
|
2026-10-16 21:14:58 +02:00
|
|
|
|
data.FixedPosition + OrbitData.Evaluate(frac(data.Phase)) * data.Distance)); // a root's stationary place: Offset at an infinite period
|
|
|
|
|
|
}
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
Wells = WellTree.Build(wellNodes, settings, pack.Radius);
|
2026-10-15 18:11:27 +02:00
|
|
|
|
foreach (var orbit in Orbits)
|
|
|
|
|
|
orbit.Value.Period = orbit.Value.Data.Parent.IsSet()
|
|
|
|
|
|
? PeriodAround(orbit.Value.Data.Parent.Key, orbit.Value.Data.Distance, orbit.Key)
|
|
|
|
|
|
: float.PositiveInfinity;
|
|
|
|
|
|
foreach (var belt in AsteroidBelts)
|
|
|
|
|
|
{
|
|
|
|
|
|
var beltOrbit = Planets[belt.Key].Orbit.Key;
|
|
|
|
|
|
var beltParent = Orbits[beltOrbit].Data.Parent.Key;
|
|
|
|
|
|
for (var i = 0; i < belt.Value.Periods.Length; i++)
|
2026-10-18 16:28:42 +02:00
|
|
|
|
belt.Value.Periods[i] = belt.Value.Data.RidesRail
|
|
|
|
|
|
? Orbits[beltOrbit].Period // a swarm's rocks ride their rail (trojans-ride-host-period-with-stations)
|
|
|
|
|
|
: PeriodAround(beltParent, belt.Value.Data.Asteroids[i].Distance, beltOrbit);
|
2026-10-15 18:11:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-16 04:13:43 +02:00
|
|
|
|
PopulatedRadius = ComputePopulatedRadius();
|
|
|
|
|
|
|
2026-10-15 18:11:27 +02:00
|
|
|
|
// Aetheria gravity (target invariant zone-valid-before-tick): every orbit is placed at the pack's time
|
|
|
|
|
|
// when the zone is built, so each well, light and body pose read before the first Update is where the
|
|
|
|
|
|
// body is. Velocity stays zero until the first Update differences two placed positions.
|
|
|
|
|
|
foreach (var orbit in Orbits)
|
|
|
|
|
|
GetOrbitPosition(orbit.Key);
|
|
|
|
|
|
|
|
|
|
|
|
// BeltTargets copies the belts' periods, so it is built after them.
|
|
|
|
|
|
foreach (var belt in AsteroidBelts)
|
|
|
|
|
|
Targets.Add(new BeltTargets(this, belt.Key, belt.Value.Data, settings, _time));
|
|
|
|
|
|
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
// Cut 2 (docs/mining-cut.md): read after belts are built so a later ChunkExists/ChunkRadius call has
|
|
|
|
|
|
// Planets/AsteroidBelts populated. A pack from before key 6 existed has ChunkWear == null (nullable
|
|
|
|
|
|
// persistence rule); that reads as no wear rather than throwing.
|
|
|
|
|
|
if (pack.ChunkWear != null)
|
|
|
|
|
|
foreach (var wear in pack.ChunkWear)
|
|
|
|
|
|
_wear[new ChunkId(wear.Field, wear.Index)] = new ChunkWear { Damage = wear.Damage, BrokenUntil = wear.BrokenUntil };
|
|
|
|
|
|
|
2026-10-21 19:01:41 +02:00
|
|
|
|
var ordinal = 0u;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
foreach (var entityPack in pack.Entities)
|
2021-03-13 03:23:58 +00:00
|
|
|
|
{
|
2026-10-21 19:01:41 +02:00
|
|
|
|
ordinal++;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var entity = EntitySerializer.Unpack(_itemManager, this, entityPack);
|
2026-09-30 12:32:59 +02:00
|
|
|
|
var ship = entity as Ship;
|
|
|
|
|
|
Admit(entity, piloted: ship is {IsPlayerShip: false});
|
2026-10-21 19:01:41 +02:00
|
|
|
|
// Generation placement, not admission: a ship packed at the origin is scattered across the zone, on the
|
|
|
|
|
|
// zone's own dice, so nothing drawn from the item stream can move it.
|
|
|
|
|
|
if (ship is {IsPlayerShip: false} && lengthsq(ship.Position) < 1)
|
|
|
|
|
|
ship.Position = SimulationDice.For(CombatSeed, SimulationDice.ScatterStream, ordinal)
|
|
|
|
|
|
.NextFloat3(float3(-pack.Radius * .5f), float3(pack.Radius * .5f));
|
2021-03-13 03:23:58 +00:00
|
|
|
|
}
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
|
|
|
|
|
// TODO: Associate planets with stored entities for planetary colonies
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-13 02:13:43 +02:00
|
|
|
|
// The one admission into this zone (docs/scenarios-cut.md, 2.2): the only writer of membership and the only
|
2026-09-30 12:32:59 +02:00
|
|
|
|
// creator of agents. Construction, new-run staging (RunStart), warp arrival, undock and the console's spawned
|
|
|
|
|
|
// turret all join through here. A piloted entity gets the zone's Minion; an unpiloted one gets no agent at all.
|
2026-10-13 02:13:43 +02:00
|
|
|
|
// Only a ship can be piloted. The death watch is installed here, for every join, whichever caller admits it.
|
|
|
|
|
|
// An entity that is already a member is refused: it would hold a second watch and a second agent.
|
LockOn: the hostile is ordered to close on the player; the warning is measured against the poses it was stamped from
ScenarioStage.Approach(entity, target) states a piloted entity's task as a follow task on the target, passed through Zone.Admit to the flight as the member's baseline task; it replaces Patrol(entity, orbit) and the orbit index, and RunStart's post-admit circuit overwrite. LockOn stages at .6, the quality Crossing and the other RunStart scenarios use (its brief states none), not .8 picked by outcome.
The bearing check keeps well-law r4's intent (measure against the hostile's actual pose, not the staged line) but reads the poses of the tick before the paint was first seen, which is the tick it was stamped in, with a .05 deg limit for float error; the live-pose check at .5 deg spent its budget on the paint inbox's deliberate one-tick delay. LockOnHoldsUnderOnePercentOfEachStagedInput moves hostile x, y and quality by 1% each way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:51:33 +02:00
|
|
|
|
// A piloted entity starts on the zone's drawn patrol circuit unless the caller states its task (a scenario's approach).
|
|
|
|
|
|
public void Admit(Entity entity, bool piloted, AgentTask task = null)
|
2026-09-30 12:32:59 +02:00
|
|
|
|
{
|
2026-10-13 02:13:43 +02:00
|
|
|
|
if (_entities.Contains(entity)) throw new InvalidOperationException("entity: already a member of this zone");
|
|
|
|
|
|
_entities.Add(entity);
|
|
|
|
|
|
_deathWatches.Add(entity, entity.Death.Subscribe(_ => { DropLoot(entity); Remove(entity); entity.Deactivate(); }));
|
2026-09-30 12:32:59 +02:00
|
|
|
|
entity.Activate();
|
LockOn: the hostile is ordered to close on the player; the warning is measured against the poses it was stamped from
ScenarioStage.Approach(entity, target) states a piloted entity's task as a follow task on the target, passed through Zone.Admit to the flight as the member's baseline task; it replaces Patrol(entity, orbit) and the orbit index, and RunStart's post-admit circuit overwrite. LockOn stages at .6, the quality Crossing and the other RunStart scenarios use (its brief states none), not .8 picked by outcome.
The bearing check keeps well-law r4's intent (measure against the hostile's actual pose, not the staged line) but reads the poses of the tick before the paint was first seen, which is the tick it was stamped in, with a .05 deg limit for float error; the live-pose check at .5 deg spent its budget on the paint inbox's deliberate one-tick delay. LockOnHoldsUnderOnePercentOfEachStagedInput moves hostile x, y and quality by 1% each way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:51:33 +02:00
|
|
|
|
if (piloted) Agents.Add(CreateAgent((Ship) entity, task));
|
2026-09-30 12:32:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 21:10:09 +02:00
|
|
|
|
// The one removal from this zone (death, dock, jump): membership and its death watch leave together, and so does the
|
|
|
|
|
|
// entity's give, which ends GiverGone unless a dock already ended it. False, and nothing changes, when the entity is
|
|
|
|
|
|
// not a member. The caller deactivates; Remove does not.
|
2026-10-13 02:13:43 +02:00
|
|
|
|
public bool Remove(Entity entity)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_entities.Remove(entity)) return false;
|
2026-10-22 21:10:09 +02:00
|
|
|
|
CargoTransfer.End(entity, GiveEnd.GiverGone);
|
2026-10-13 02:13:43 +02:00
|
|
|
|
_deathWatches.Remove(entity, out var watch);
|
|
|
|
|
|
watch.Dispose();
|
2026-10-13 05:00:22 +02:00
|
|
|
|
foreach (var floating in FloatingItems)
|
|
|
|
|
|
if (ReferenceEquals(floating.Holder, entity)) Free(floating.Id);
|
2026-10-13 02:13:43 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 15:39:02 +02:00
|
|
|
|
private static object SideOf(Ship ship) => (object) ship.Faction ?? ship;
|
|
|
|
|
|
|
LockOn: the hostile is ordered to close on the player; the warning is measured against the poses it was stamped from
ScenarioStage.Approach(entity, target) states a piloted entity's task as a follow task on the target, passed through Zone.Admit to the flight as the member's baseline task; it replaces Patrol(entity, orbit) and the orbit index, and RunStart's post-admit circuit overwrite. LockOn stages at .6, the quality Crossing and the other RunStart scenarios use (its brief states none), not .8 picked by outcome.
The bearing check keeps well-law r4's intent (measure against the hostile's actual pose, not the staged line) but reads the poses of the tick before the paint was first seen, which is the tick it was stamped in, with a .05 deg limit for float error; the live-pose check at .5 deg spent its budget on the paint inbox's deliberate one-tick delay. LockOnHoldsUnderOnePercentOfEachStagedInput moves hostile x, y and quality by 1% each way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:51:33 +02:00
|
|
|
|
private Agent CreateAgent(Ship ship, AgentTask stated)
|
2021-03-18 00:36:34 +00:00
|
|
|
|
{
|
2026-10-22 15:39:02 +02:00
|
|
|
|
var key = SideOf(ship);
|
2026-10-08 14:18:41 +02:00
|
|
|
|
if (!_flights.TryGetValue(key, out var flight))
|
|
|
|
|
|
_flights[key] = flight = new Flight(ship.Faction?.Doctrine, _itemManager.GameplaySettings);
|
2026-10-17 20:46:37 +02:00
|
|
|
|
var id = NextAgentId();
|
|
|
|
|
|
var agent = new Minion(ship, flight.Combatant) { Id = id };
|
LockOn: the hostile is ordered to close on the player; the warning is measured against the poses it was stamped from
ScenarioStage.Approach(entity, target) states a piloted entity's task as a follow task on the target, passed through Zone.Admit to the flight as the member's baseline task; it replaces Patrol(entity, orbit) and the orbit index, and RunStart's post-admit circuit overwrite. LockOn stages at .6, the quality Crossing and the other RunStart scenarios use (its brief states none), not .8 picked by outcome.
The bearing check keeps well-law r4's intent (measure against the hostile's actual pose, not the staged line) but reads the poses of the tick before the paint was first seen, which is the tick it was stamped in, with a .05 deg limit for float error; the live-pose check at .5 deg spent its budget on the paint inbox's deliberate one-tick delay. LockOnHoldsUnderOnePercentOfEachStagedInput moves hostile x, y and quality by 1% each way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:51:33 +02:00
|
|
|
|
flight.Join(agent, stated ?? PatrolCircuit(id));
|
2021-03-18 00:36:34 +00:00
|
|
|
|
return agent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
LockOn: the hostile is ordered to close on the player; the warning is measured against the poses it was stamped from
ScenarioStage.Approach(entity, target) states a piloted entity's task as a follow task on the target, passed through Zone.Admit to the flight as the member's baseline task; it replaces Patrol(entity, orbit) and the orbit index, and RunStart's post-admit circuit overwrite. LockOn stages at .6, the quality Crossing and the other RunStart scenarios use (its brief states none), not .8 picked by outcome.
The bearing check keeps well-law r4's intent (measure against the hostile's actual pose, not the staged line) but reads the poses of the tick before the paint was first seen, which is the tick it was stamped in, with a .05 deg limit for float error; the live-pose check at .5 deg spent its budget on the paint inbox's deliberate one-tick delay. LockOnHoldsUnderOnePercentOfEachStagedInput moves hostile x, y and quality by 1% each way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:51:33 +02:00
|
|
|
|
private PatrolOrbitsTask PatrolCircuit(uint id)
|
|
|
|
|
|
{
|
|
|
|
|
|
var dice = SimulationDice.For(CombatSeed, SimulationDice.PatrolStream, id);
|
|
|
|
|
|
return new PatrolOrbitsTask { Circuit = Orbits.OrderBy(_ => dice.NextFloat()).Take(4).Select(x => x.Key).ToArray() };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public ZonePack PackZone()
|
2026-06-21 09:37:54 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
return new ZonePack
|
|
|
|
|
|
{
|
|
|
|
|
|
Radius = Pack.Radius,
|
|
|
|
|
|
Mass = Pack.Mass,
|
2026-10-13 02:13:43 +02:00
|
|
|
|
Entities = _entities.Select(EntitySerializer.Pack).ToList(),
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
Orbits = Orbits.Keys.Select(key => new CultRecordRef<OrbitData>(key)).ToList(),
|
|
|
|
|
|
Planets = Planets.Keys.Select(key => new CultRecordRef<BodyData>(key)).ToList(),
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
Time = _time,
|
2026-10-07 06:41:33 +02:00
|
|
|
|
BossSpawned = Pack.BossSpawned,
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
// Cut 2 (docs/mining-cut.md): drop entries that carry no live information -- healed (no damage) and
|
|
|
|
|
|
// not currently broken -- so a save never accumulates wear rows for chunks nobody has touched since
|
|
|
|
|
|
// they last respawned.
|
|
|
|
|
|
ChunkWear = _wear
|
|
|
|
|
|
.Where(kv => !IsExpired(kv.Value))
|
|
|
|
|
|
.Select(kv => new ChunkWearPack { Field = kv.Key.Field, Index = kv.Key.Index, Damage = kv.Value.Damage, BrokenUntil = kv.Value.BrokenUntil })
|
|
|
|
|
|
.ToList()
|
2026-08-10 14:57:50 +01:00
|
|
|
|
};
|
2026-06-21 09:37:54 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
private bool IsExpired(ChunkWear wear) => wear.Damage <= 0f && (!wear.BrokenUntil.HasValue || wear.BrokenUntil.Value <= _time);
|
|
|
|
|
|
|
2021-03-27 10:03:24 +00:00
|
|
|
|
public void Update(float deltaTime)
|
2020-11-27 22:47:33 +00:00
|
|
|
|
{
|
2021-03-27 10:03:24 +00:00
|
|
|
|
_time += deltaTime;
|
2020-12-03 17:32:34 +00:00
|
|
|
|
_updatedOrbits.Clear();
|
2026-10-14 21:19:09 +02:00
|
|
|
|
// Every previous position is taken before any orbit is placed: GetOrbitPosition places a parent as a
|
|
|
|
|
|
// side effect of placing its child, so differencing inside one loop would depend on dictionary order.
|
2026-08-10 14:57:50 +01:00
|
|
|
|
foreach (var orbit in Orbits)
|
|
|
|
|
|
orbit.Value.PreviousPosition = orbit.Value.Position;
|
2026-10-14 21:19:09 +02:00
|
|
|
|
foreach (var orbit in Orbits)
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
orbit.Value.Position = GetOrbitPosition(orbit.Key);
|
|
|
|
|
|
orbit.Value.Velocity = (orbit.Value.Position - orbit.Value.PreviousPosition) / deltaTime;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-08 14:18:41 +02:00
|
|
|
|
foreach (var flight in _flights.Values) flight.Update(deltaTime);
|
2026-10-22 00:32:11 +02:00
|
|
|
|
// Every pilot's command is applied by PilotApplier, once per step. Entities with no agent (the player, a scripted
|
|
|
|
|
|
// pilot, a lone ship) first, as the player's input lands before the step; then each agent applies right after it
|
|
|
|
|
|
// decides, so the next agent sees it as it saw the direct writes this replaced.
|
|
|
|
|
|
var agents = Agents.ToArray();
|
|
|
|
|
|
var piloted = new HashSet<Entity>();
|
|
|
|
|
|
foreach (var agent in agents) piloted.Add(agent.Ship);
|
|
|
|
|
|
foreach (var entity in Entities.ToArray())
|
|
|
|
|
|
if (!piloted.Contains(entity)) PilotApplier.Apply(entity);
|
|
|
|
|
|
foreach (var agent in agents)
|
|
|
|
|
|
{
|
2021-03-09 02:26:58 +00:00
|
|
|
|
agent.Update(deltaTime);
|
2026-10-22 00:32:11 +02:00
|
|
|
|
PilotApplier.Apply(agent.Ship);
|
|
|
|
|
|
}
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
|
2026-10-10 23:29:27 +02:00
|
|
|
|
var entities = Entities.ToArray();
|
2026-10-11 15:22:29 +02:00
|
|
|
|
foreach (var entity in entities) entity.PublishInbox();
|
2026-10-10 23:29:27 +02:00
|
|
|
|
foreach (var entity in entities) entity.Update(deltaTime);
|
2026-10-11 20:34:58 +02:00
|
|
|
|
foreach (var entity in entities) entity.Sense();
|
2026-10-11 22:03:20 +02:00
|
|
|
|
_departures.Clear();
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
|
|
2026-10-22 16:38:17 +02:00
|
|
|
|
CargoTransfer.Step(this, deltaTime);
|
2026-10-07 19:47:21 +02:00
|
|
|
|
StepMines(deltaTime);
|
2026-10-09 20:22:00 +02:00
|
|
|
|
StepClouds(deltaTime);
|
2026-10-12 04:00:08 +02:00
|
|
|
|
StepFloatingItems(deltaTime);
|
2026-10-07 19:47:21 +02:00
|
|
|
|
|
2026-10-12 05:46:32 +02:00
|
|
|
|
// Hull contact (cut sim-contact): after every entity has moved and the mines and clouds have taken
|
|
|
|
|
|
// their toll, so only hulls still in the zone touch, and before shots resolve against the result.
|
|
|
|
|
|
Contacts.Step(this);
|
|
|
|
|
|
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
|
// Cut 3: after every entity has had its chance to fire this tick, age and resolve the shots that
|
|
|
|
|
|
// firing queued. A shot fired this tick with a flight time shorter than CommitHorizon commits and
|
|
|
|
|
|
// resolves in this same call.
|
|
|
|
|
|
FireControl.Step(this, deltaTime);
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
|
2026-10-07 19:47:21 +02:00
|
|
|
|
public void Lay(Mine mine)
|
|
|
|
|
|
{
|
|
|
|
|
|
mine.LaidAt = _time;
|
|
|
|
|
|
Mines.Add(mine);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-09 20:22:00 +02:00
|
|
|
|
public void Vent(VapourCloud cloud)
|
|
|
|
|
|
{
|
|
|
|
|
|
cloud.Id = NextBodyId();
|
|
|
|
|
|
cloud.VentedAt = _time;
|
|
|
|
|
|
Clouds.Add(cloud);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-12 04:00:08 +02:00
|
|
|
|
// The one adder of floating items: death drops and jettison both come here. The item drifts as a KinematicBody
|
|
|
|
|
|
// launched at `velocity` and expires after FloatingItemLifetime.
|
|
|
|
|
|
public FloatingItem Release(ItemInstance item, float2 position, float2 velocity)
|
|
|
|
|
|
{
|
|
|
|
|
|
var floating = new FloatingItem
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = NextBodyId(), Item = item, Body = new KinematicBody { Position = position, Velocity = velocity },
|
|
|
|
|
|
ReleasedAt = _time, Lifetime = _itemManager.GameplaySettings.FloatingItemLifetime
|
|
|
|
|
|
};
|
|
|
|
|
|
FloatingItems.Add(floating);
|
|
|
|
|
|
return floating;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The one commit of a pickup: the first of the taker's cargo bays that accepts the item stores it and the item
|
|
|
|
|
|
// stops floating. Otherwise nothing changes.
|
2026-10-13 05:00:22 +02:00
|
|
|
|
// An item held by another entity is refused: one holder per item holds at the commit as well.
|
2026-10-12 04:00:08 +02:00
|
|
|
|
public bool TryPickUp(Entity taker, FloatingBodyId id)
|
|
|
|
|
|
{
|
|
|
|
|
|
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
2026-10-13 05:00:22 +02:00
|
|
|
|
if (floating == null || (floating.Holder != null && !ReferenceEquals(floating.Holder, taker))) return false;
|
|
|
|
|
|
if (!taker.CargoBays.Any(bay => bay.TryStore(floating.Item))) return false;
|
2026-10-12 04:00:08 +02:00
|
|
|
|
FloatingItems.Remove(floating);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-13 05:00:22 +02:00
|
|
|
|
// The tractor beam's claim on a floating item: false if it is absent or already held. The item stays where it is
|
|
|
|
|
|
// until its pull begins; StepFloatingItems places it from then on.
|
|
|
|
|
|
public bool TryHold(Entity holder, FloatingBodyId id)
|
|
|
|
|
|
{
|
|
|
|
|
|
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
|
|
|
|
|
if (floating == null || floating.Holder != null) return false;
|
|
|
|
|
|
floating.Holder = holder;
|
|
|
|
|
|
floating.HeldFrom = floating.Body.Position;
|
|
|
|
|
|
floating.HeldProgress = 0;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// How far along its pull to the holder a held item is, 0..1.
|
|
|
|
|
|
public void SetHeldProgress(FloatingBodyId id, float progress)
|
|
|
|
|
|
{
|
|
|
|
|
|
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
|
|
|
|
|
if (floating?.Holder != null) floating.HeldProgress = saturate(progress);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Lets a held item go: it floats on from where it is with the Velocity and Drift it had.
|
|
|
|
|
|
public void Free(FloatingBodyId id)
|
|
|
|
|
|
{
|
|
|
|
|
|
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
|
|
|
|
|
|
if (floating == null) return;
|
|
|
|
|
|
floating.Holder = null;
|
|
|
|
|
|
floating.HeldProgress = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-12 04:00:08 +02:00
|
|
|
|
// Called from the death subscription, before the entity leaves the zone: one generator per death. Each equipped
|
|
|
|
|
|
// item but the hull drops on a LootDropProbability draw; every cargo stack drops whole. Each drop leaves from the
|
|
|
|
|
|
// dead entity's position at a random heading. Draw order: Equipment order (the probability draw, then the heading
|
|
|
|
|
|
// of a dropped item), then each cargo bay's stacks in CargoBays order (a heading each).
|
|
|
|
|
|
private void DropLoot(Entity dead)
|
|
|
|
|
|
{
|
|
|
|
|
|
var settings = _itemManager.GameplaySettings;
|
2026-10-18 17:30:50 +02:00
|
|
|
|
var random = SimulationDice.For(DropSeed, SimulationDice.LootStream, ++_deaths);
|
2026-10-12 04:00:08 +02:00
|
|
|
|
var at = new float2(dead.Position.x, dead.Position.z);
|
|
|
|
|
|
float2 Heading() { var angle = random.NextFloat() * 2f * PI; return float2(cos(angle), sin(angle)); }
|
|
|
|
|
|
foreach (var equipped in dead.Equipment.ToArray())
|
|
|
|
|
|
{
|
|
|
|
|
|
if (equipped == dead.EquippedHull || !(random.NextFloat() < settings.LootDropProbability)) continue;
|
|
|
|
|
|
Release(equipped.EquippableItem, at, Heading() * settings.LootDropVelocity);
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach (var bay in dead.CargoBays.ToArray())
|
|
|
|
|
|
foreach (var stack in bay.Cargo.Keys.ToArray())
|
|
|
|
|
|
Release(stack, at, Heading() * settings.LootDropVelocity);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// After StepClouds and before FireControl.Step: each floating item drifts as a KinematicBody on the zone's force
|
2026-10-13 05:00:22 +02:00
|
|
|
|
// and is removed once its lifetime has run out. A held item is neither integrated nor expired: it is placed
|
|
|
|
|
|
// between where it was claimed and its holder, now that the entities have moved.
|
2026-10-13 22:25:49 +02:00
|
|
|
|
// The curve a held item rides from where it was claimed to its holder: the one owner, which the test scene's
|
|
|
|
|
|
// stand-in sim and tendril call rather than spell again.
|
|
|
|
|
|
public static float HeldPull(float progress) => smoothstep(0f, 1f, progress);
|
|
|
|
|
|
|
2026-10-12 04:00:08 +02:00
|
|
|
|
private void StepFloatingItems(float deltaTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
var settings = _itemManager.GameplaySettings;
|
|
|
|
|
|
foreach (var floating in FloatingItems.ToArray())
|
|
|
|
|
|
{
|
2026-10-13 05:00:22 +02:00
|
|
|
|
if (floating.Holder != null)
|
|
|
|
|
|
{
|
2026-10-13 22:25:49 +02:00
|
|
|
|
floating.Body.Position = lerp(floating.HeldFrom, floating.Holder.Position.xz, HeldPull(floating.HeldProgress));
|
2026-10-13 05:00:22 +02:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-10-12 04:00:08 +02:00
|
|
|
|
floating.Body.Step(deltaTime, GetForce(floating.Body.Position), settings);
|
|
|
|
|
|
if (floating.ReleasedAt + floating.Lifetime <= _time) FloatingItems.Remove(floating);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-09 20:22:00 +02:00
|
|
|
|
// The fraction of a sight line's signal that survives the clouds between `a` and `b`: the product of
|
|
|
|
|
|
// (1 - opacity now) over every cloud whose disc the segment touches, so 1f when none does. Derived from the
|
|
|
|
|
|
// live clouds at each call and stored nowhere; Sensor.Gain is where it enters detection.
|
|
|
|
|
|
public float Obscuration(float2 a, float2 b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var through = 1f;
|
|
|
|
|
|
for (var i = 0; i < Clouds.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cloud = Clouds[i];
|
|
|
|
|
|
if (cloud.Touches(a, b)) through *= 1f - cloud.OpacityAt(_time);
|
|
|
|
|
|
}
|
|
|
|
|
|
return through;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// After StepMines: each cloud drifts as a KinematicBody on the zone's force, as a mine does, and is removed
|
|
|
|
|
|
// once its lifetime has run out. Nothing else removes a cloud.
|
|
|
|
|
|
private void StepClouds(float deltaTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
var settings = _itemManager.GameplaySettings;
|
|
|
|
|
|
foreach (var cloud in Clouds.ToArray())
|
|
|
|
|
|
{
|
|
|
|
|
|
cloud.Body.Step(deltaTime, GetForce(cloud.Body.Position), settings);
|
|
|
|
|
|
if (cloud.VentedAt + cloud.Lifetime <= _time) Clouds.Remove(cloud);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 19:47:21 +02:00
|
|
|
|
// After the entity loop and before FireControl.Step, in lay order: each mine drifts as a KinematicBody, is
|
|
|
|
|
|
// triggered by the first eligible hull its armed disc touches, and detonates through FireControl.Detonate at
|
|
|
|
|
|
// its trigger plus fuse delay or at its lifetime, whatever its layer does (ruling mine-owner-leash). The
|
|
|
|
|
|
// blast reads no faction; only the trigger does.
|
|
|
|
|
|
private void StepMines(float deltaTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
var settings = _itemManager.GameplaySettings;
|
|
|
|
|
|
foreach (var mine in Mines.ToArray())
|
|
|
|
|
|
{
|
|
|
|
|
|
mine.Body.Step(deltaTime, GetForce(mine.Body.Position), settings);
|
|
|
|
|
|
|
2026-10-07 20:17:47 +02:00
|
|
|
|
if (mine.TriggeredAt == null && mine.Armed(_time) &&
|
|
|
|
|
|
Entities.Any(entity => mine.Triggers(entity) && FireControl.Touches(entity, mine.Body.Position, mine.BlastRadius)))
|
|
|
|
|
|
mine.TriggeredAt = _time;
|
2026-10-07 19:47:21 +02:00
|
|
|
|
|
|
|
|
|
|
var fused = mine.TriggeredAt + mine.FuseDelay <= _time;
|
|
|
|
|
|
if (!fused && mine.LaidAt + mine.Lifetime > _time) continue;
|
|
|
|
|
|
Mines.Remove(mine);
|
|
|
|
|
|
FireControl.Detonate(this, mine.Body.Position, mine.BlastRadius, mine.Damage, mine.DamageType);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-11-27 22:47:33 +00:00
|
|
|
|
// Determine orbital position recursively, caching parent positions to avoid repeated calculations
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
public float2 GetOrbitPosition(CultRecordKey orbitID)
|
2020-11-27 22:47:33 +00:00
|
|
|
|
{
|
2020-12-01 00:16:11 +00:00
|
|
|
|
// Root orbit is fixed at origin
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
if(!orbitID.IsSet())
|
2026-08-10 14:57:50 +01:00
|
|
|
|
return float2.zero;
|
|
|
|
|
|
if (!Orbits.ContainsKey(orbitID))
|
2021-02-11 03:14:20 +00:00
|
|
|
|
{
|
|
|
|
|
|
Log?.Invoke("Requested orbit is not part of this zone!");
|
2026-08-10 14:57:50 +01:00
|
|
|
|
return float2.zero;
|
2021-02-11 03:14:20 +00:00
|
|
|
|
}
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (!_updatedOrbits.Contains(orbitID))
|
2020-11-27 22:47:33 +00:00
|
|
|
|
{
|
2026-10-15 22:39:06 +02:00
|
|
|
|
Orbits[orbitID].Position = RailAt(orbitID, _time);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
_updatedOrbits.Add(orbitID);
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
return Orbits[orbitID].Position;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
// The rail law, once: where an orbit sits from its parent's node at a time. An infinite period (a parentless orbit,
|
|
|
|
|
|
// or one with no field to circle in) leaves the orbit at its phase: frac(t / inf) is 0.
|
|
|
|
|
|
private static float2 Offset(Orbit orbit, double time)
|
|
|
|
|
|
{
|
|
|
|
|
|
var pos = float2.zero;
|
|
|
|
|
|
if (orbit.Period > .01f)
|
|
|
|
|
|
{
|
|
|
|
|
|
var phase = (float) frac(time / orbit.Period);
|
|
|
|
|
|
pos = OrbitData.Evaluate(frac(phase + orbit.Data.Phase)) * orbit.Data.Distance;
|
|
|
|
|
|
|
|
|
|
|
|
if (float.IsNaN(pos.x))
|
|
|
|
|
|
pos = float2.zero;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return pos;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The same rail law at any time, with no memo read or written: a parent is its own rail at that time.
|
|
|
|
|
|
private float2 RailAt(CultRecordKey orbitID, double time)
|
|
|
|
|
|
{
|
2026-10-16 00:50:29 +02:00
|
|
|
|
if (!orbitID.IsSet())
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
return float2.zero;
|
2026-10-16 00:50:29 +02:00
|
|
|
|
if (!Orbits.TryGetValue(orbitID, out var orbit))
|
|
|
|
|
|
{
|
|
|
|
|
|
Log?.Invoke("Requested orbit is not part of this zone!");
|
|
|
|
|
|
return float2.zero;
|
|
|
|
|
|
}
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
var parentPosition = !orbit.Data.Parent.IsSet() ? orbit.Data.FixedPosition : RailAt(orbit.Data.Parent.Key, time);
|
|
|
|
|
|
return parentPosition + Offset(orbit, time);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Where an orbit is at a time: pure, writes nothing. At the step's own time it is the step's memo (GetOrbitPosition);
|
|
|
|
|
|
// at any other time (a presenter's drawn time) it is the rail law evaluated there.
|
|
|
|
|
|
public float2 OrbitPositionAt(CultRecordKey orbitID, double time)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!orbitID.IsSet())
|
|
|
|
|
|
return float2.zero;
|
|
|
|
|
|
if (!Orbits.ContainsKey(orbitID))
|
|
|
|
|
|
{
|
|
|
|
|
|
Log?.Invoke("Requested orbit is not part of this zone!");
|
|
|
|
|
|
return float2.zero;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return time == _time ? GetOrbitPosition(orbitID) : RailAt(orbitID, time);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
/// <summary>The zone's gravity: the terms of its orbit tree (WellTree owns which exist and the walk that sums them).</summary>
|
|
|
|
|
|
public WellTree Wells { get; }
|
2026-10-15 18:11:27 +02:00
|
|
|
|
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
private readonly Func<CultRecordKey, double, float2> _orbitPositionAt;
|
2026-10-15 19:05:03 +02:00
|
|
|
|
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
// The one owner of a rail's period (Orbit.Period, AsteroidBelt.Periods): 2 pi sqrt(d / a), a being the pull the field
|
|
|
|
|
|
// gives at distance d from the node being orbited (WellTree.PullAround). No pull gives an infinite period.
|
2026-10-15 18:11:27 +02:00
|
|
|
|
private float PeriodAround(CultRecordKey parentOrbit, float distance, CultRecordKey self)
|
|
|
|
|
|
{
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
var a = Wells.PullAround(parentOrbit, distance, self);
|
2026-10-15 18:11:27 +02:00
|
|
|
|
return a > 0 && distance > 0 ? 2 * PI * sqrt(distance / a) : float.PositiveInfinity;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
public float2 GetOrbitVelocity(CultRecordKey orbit)
|
2026-06-16 16:07:20 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (Orbits.ContainsKey(orbit))
|
|
|
|
|
|
return Orbits[orbit].Velocity;
|
|
|
|
|
|
return float2.zero;
|
2026-06-16 16:07:20 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
// Cut 2 (docs/mining-cut.md): the index is in range for its field and the chunk is not broken at zone time.
|
|
|
|
|
|
// Respawn is a comparison against zone time, never a per-tick loop or a write on read.
|
|
|
|
|
|
public bool ChunkExists(ChunkId chunk)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!(Planets.TryGetValue(chunk.Field, out var body) && body is AsteroidBeltData beltData))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
if (chunk.Index < 0 || chunk.Index >= beltData.Asteroids.Length)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
return !(_wear.TryGetValue(chunk, out var wear) && wear.BrokenUntil.HasValue && wear.BrokenUntil.Value > _time);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cut 2: the size rule of AsteroidBelt.Size (Cut 1), now reading wear from its one owner (Zone) instead of
|
|
|
|
|
|
// per-belt dictionaries. A broken chunk (BrokenUntil in the future) has no size; an unbroken, undamaged
|
|
|
|
|
|
// chunk reads its authored size; a damaged one shrinks toward it.
|
|
|
|
|
|
public float ChunkRadius(ChunkId chunk)
|
|
|
|
|
|
{
|
|
|
|
|
|
var belt = AsteroidBelts[chunk.Field];
|
|
|
|
|
|
if (!_wear.TryGetValue(chunk, out var wear))
|
|
|
|
|
|
return belt.UndamagedSize(chunk.Index, Settings);
|
|
|
|
|
|
if (wear.BrokenUntil.HasValue && wear.BrokenUntil.Value > _time)
|
|
|
|
|
|
return 0f;
|
|
|
|
|
|
if (wear.Damage <= 0f)
|
|
|
|
|
|
return belt.UndamagedSize(chunk.Index, Settings);
|
|
|
|
|
|
return belt.DamagedSize(chunk.Index, wear.Damage, Settings);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cut 2: the only writer of chunk wear. Returns whether this hit broke the chunk. The break threshold is
|
|
|
|
|
|
// strictly greater-than, matching the deleted per-tick miner's own comparison against accumulated damage:
|
|
|
|
|
|
// damage exactly equal to hitpoints does not yet break the chunk, only damage that exceeds them does.
|
2026-09-25 12:49:57 +02:00
|
|
|
|
// A chunk that is currently broken (zone time < BrokenUntil) absorbs nothing: this hit did not break it
|
|
|
|
|
|
// (it was already broken), and its wear stays zeroed until it respawns and starts fresh.
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
public bool Wear(ChunkId chunk, float damage)
|
|
|
|
|
|
{
|
2026-09-25 12:49:57 +02:00
|
|
|
|
_wear.TryGetValue(chunk, out var wear);
|
|
|
|
|
|
if (wear.BrokenUntil.HasValue && wear.BrokenUntil.Value > _time)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
var beltData = (AsteroidBeltData) Planets[chunk.Field];
|
|
|
|
|
|
var size = beltData.Asteroids[chunk.Index].Size;
|
|
|
|
|
|
var hitpoints = Settings.AsteroidHitpoints.Evaluate(size);
|
|
|
|
|
|
|
|
|
|
|
|
var newDamage = wear.Damage + damage;
|
|
|
|
|
|
|
|
|
|
|
|
if (newDamage > hitpoints)
|
|
|
|
|
|
{
|
|
|
|
|
|
var respawnTime = Settings.AsteroidRespawnTime.Evaluate(size);
|
|
|
|
|
|
_wear[chunk] = new ChunkWear { Damage = 0f, BrokenUntil = _time + respawnTime };
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_wear[chunk] = new ChunkWear { Damage = newDamage, BrokenUntil = null };
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-06-21 09:37:54 +01:00
|
|
|
|
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
// Cut 1 (docs/mining-cut.md): a chunk's pose is a pure function of zone time, computed when asked. No stored
|
|
|
|
|
|
// pose survives a tick; the sim and the renderer both read it fresh through here.
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
public float4 ChunkPose(CultRecordKey belt, int index) => ChunkPose(belt, index, _time);
|
|
|
|
|
|
|
|
|
|
|
|
// The same pose at any time: the sim reads it at _time, a presenter at the drawn time (DrawAhead.Time).
|
|
|
|
|
|
public float4 ChunkPose(CultRecordKey belt, int index, double time)
|
2026-06-16 15:08:00 +01:00
|
|
|
|
{
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
var beltData = Planets[belt] as AsteroidBeltData;
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
var parentPosition = OrbitPositionAt(Orbits[beltData.Orbit.Key].Data.Parent.Key, time);
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
var size = ChunkRadius(new ChunkId(belt, index));
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
return AsteroidBelts[belt].Pose(index, time, parentPosition, size);
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
}
|
2021-01-17 23:26:53 +00:00
|
|
|
|
|
Mining Cut 3: field kinds, chunk visibility, the chunk query, and TargetRef
- FieldKindData, a catalog document (Q11): a kind's reflectivity per schematic
cell and its generation weight. AetheriaStores routes it to the catalog.
- AsteroidBeltData.Kind, key 10 (Q16 B). FieldKinds.Assign is the one pick (a
weighted choice by a stable hash of the belt's key); FieldKinds.Ensure gives
a kindless belt its kind and saves it. ZoneGenerator calls it at generation
and Zone calls it on first load, so an old belt is assigned once by the same
rule and keeps its kind after later catalog edits.
- Zone.ChunkVisibility: Reflector's rule for a chunk -- kind cross-section x
area in schematic cells x the light on it. Stored nowhere; a worn chunk dims,
a broken one or a kindless belt's is dark.
- Zone.ChunksNear: the one chunk query, skipping belts whose annulus cannot
reach. AsteroidBelt gains InnerRadius for that skip.
- TargetRef: an entity, a chunk, or none, with value equality and no ==.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:24 +02:00
|
|
|
|
// Mining Cut 3 (docs/mining-cut-refresh.md): how bright a chunk is -- Reflector's rule (cross-section × the light
|
|
|
|
|
|
// on it), with the field kind's reflectivity per schematic cell times the chunk's area in cells. Computed when
|
|
|
|
|
|
// asked; nothing about it is stored. A worn chunk shrinks and dims; a broken one, or one of a kindless belt, is dark.
|
|
|
|
|
|
public float ChunkVisibility(ChunkId chunk)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!ChunkExists(chunk)) return 0f;
|
|
|
|
|
|
var kind = _itemManager.ItemData.Get(((AsteroidBeltData) Planets[chunk.Field]).Kind);
|
|
|
|
|
|
if (kind == null) return 0f;
|
|
|
|
|
|
var cells = ChunkRadius(chunk) / _itemManager.GameplaySettings.SchematicCellSize;
|
|
|
|
|
|
return kind.CrossSection * PI * cells * cells * GetLight(ChunkPose(chunk.Field, chunk.Index).xy);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
public void EvaluateBelt(CultRecordKey belt, Span<float4> into, double time)
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
{
|
|
|
|
|
|
var beltData = Planets[belt] as AsteroidBeltData;
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
var parentPosition = OrbitPositionAt(Orbits[beltData.Orbit.Key].Data.Parent.Key, time);
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
var beltInstance = AsteroidBelts[belt];
|
|
|
|
|
|
for (var i = 0; i < beltData.Asteroids.Length; i++)
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
into[i] = beltInstance.Pose(i, time, parentPosition, ChunkRadius(new ChunkId(belt, i)));
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public SecurityLevel GetSecurityLevel(float2 pos)
|
2021-05-11 04:45:42 +01:00
|
|
|
|
{
|
2021-05-14 03:03:28 +01:00
|
|
|
|
if (GalaxyZone.Owner==null) return SecurityLevel.Open;
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
|
2021-05-11 04:45:42 +01:00
|
|
|
|
var security = SecurityLevel.Open;
|
|
|
|
|
|
foreach (var entity in Entities)
|
|
|
|
|
|
{
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
if (entity is OrbitalEntity orbitalEntity && orbitalEntity.SecurityRadius > 1 && entity.Faction == GalaxyZone.Owner)
|
2021-05-11 04:45:42 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
if (orbitalEntity.SecurityLevel > security && length(orbitalEntity.Position.xz - pos) < orbitalEntity.SecurityRadius * Settings.SecureAreaRadiusMultiplier)
|
2021-05-11 04:45:42 +01:00
|
|
|
|
security = orbitalEntity.SecurityLevel;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return security;
|
|
|
|
|
|
}
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
|
|
2026-10-16 04:13:43 +02:00
|
|
|
|
// The extent of the populated area, from the zone's origin: owned here; presenters read it, nothing authors it. A static
|
2026-10-16 06:11:49 +02:00
|
|
|
|
// bound on where anything can be, so it holds at every time and reads nothing the pack's Radius decides: for each body,
|
|
|
|
|
|
// its orbit chain's reach plus its Roche limit; for each belt, the chain's reach to the orbit its asteroids circle (the
|
|
|
|
|
|
// belt orbit's parent) plus the belt's own Radius, its largest asteroid Distance.
|
2026-10-16 04:13:43 +02:00
|
|
|
|
public float PopulatedRadius { get; }
|
|
|
|
|
|
|
|
|
|
|
|
private float ComputePopulatedRadius()
|
|
|
|
|
|
{
|
|
|
|
|
|
var radius = 0f;
|
|
|
|
|
|
foreach (var body in PlanetInstances.Values)
|
2026-10-16 06:11:49 +02:00
|
|
|
|
radius = max(radius, ChainReach(body.BodyData.Orbit.Key) + body.RocheLimit);
|
|
|
|
|
|
foreach (var belt in AsteroidBelts)
|
|
|
|
|
|
radius = max(radius, ChainReach(Orbits[Planets[belt.Key].Orbit.Key].Data.Parent.Key) + belt.Value.Radius);
|
|
|
|
|
|
return radius;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The most an orbit can lie from the origin: the root's offset plus every orbit Distance down the chain to it.
|
|
|
|
|
|
private float ChainReach(CultRecordKey key)
|
|
|
|
|
|
{
|
|
|
|
|
|
var reach = 0f;
|
|
|
|
|
|
while (key.IsSet() && Orbits.TryGetValue(key, out var orbit))
|
2026-10-16 04:13:43 +02:00
|
|
|
|
{
|
2026-10-16 06:11:49 +02:00
|
|
|
|
if (orbit.Data.Parent.IsSet()) reach += orbit.Data.Distance;
|
|
|
|
|
|
else reach += length(orbit.Data.FixedPosition);
|
|
|
|
|
|
key = orbit.Data.Parent.Key;
|
2026-10-16 04:13:43 +02:00
|
|
|
|
}
|
2026-10-16 06:11:49 +02:00
|
|
|
|
return reach;
|
2026-10-16 04:13:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
public float GetHeight(float2 position) => GetHeight(position, _time);
|
|
|
|
|
|
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
// The field at a time: each term sits where its node's rail puts it then. The sim reads it at _time, a presenter at
|
Draw every moving body at the drawn time T = ExactTime + Lead
Planets, belts, wells, loot, mines, stations, the chunk reticle and the flow
clock are evaluated through DrawAhead (Time, Orbit, Height, Position) at one
drawn time; the rail law lives once in Zone.Offset; OrbitPositionAt, GetHeight,
ChunkPose, EvaluateBelt and RideHeight take a time. Fixes the planet jumping
against a co-moving ship at display rates above the step rate.
Not yet built or run (Unity presenter files are verified by compile only on
Starfire).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:26:36 +02:00
|
|
|
|
// the drawn time, so a ship rides the same slope the drawn planet's well makes.
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
public float GetHeight(float2 position, double time) => Wells.Potential(position, _orbitPositionAt, time) / Settings.HeightExaggeration;
|
2020-11-29 22:50:12 +00:00
|
|
|
|
|
2021-04-12 04:20:52 +01:00
|
|
|
|
public float GetLight(float2 position)
|
|
|
|
|
|
{
|
|
|
|
|
|
var light = 0f;
|
2026-10-03 15:02:51 +02:00
|
|
|
|
foreach (var sun in _suns)
|
|
|
|
|
|
light += SunLight(sun, length(position - sun.Orbit.Position));
|
2021-04-12 04:20:52 +01:00
|
|
|
|
return light;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-03 15:02:51 +02:00
|
|
|
|
// One sun's light at `distance` from it. It never grows with distance, so the light at the nearest point of a
|
|
|
|
|
|
// region bounds the light anywhere in it (BeltTargets' visibility bound).
|
|
|
|
|
|
public static float SunLight(Sun sun, float distance) =>
|
|
|
|
|
|
distance < sun.LightRadius ? PowerPulse(distance / sun.LightRadius, 8) : 0f;
|
|
|
|
|
|
|
|
|
|
|
|
internal IReadOnlyList<Sun> Suns() => _suns;
|
|
|
|
|
|
internal float SchematicCellSize => _itemManager.GameplaySettings.SchematicCellSize;
|
|
|
|
|
|
internal float FieldCrossSection(AsteroidBeltData belt) => _itemManager.ItemData.Get(belt.Kind)?.CrossSection ?? 0f;
|
|
|
|
|
|
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
// The one source of gravitational acceleration (u/s^2): the walk of the zone's terms (WellTree). Ships, floating items,
|
|
|
|
|
|
// clouds and mines all read it, and so does Flow. It is the gradient of the potential GetHeight draws, so the two
|
|
|
|
|
|
// cannot disagree.
|
|
|
|
|
|
public float2 GetForce(float2 position) => Wells.Force(position, _orbitPositionAt, _time);
|
2020-11-29 22:50:12 +00:00
|
|
|
|
|
2026-10-13 20:06:35 +02:00
|
|
|
|
// The global flow velocity at `position` in zone time, in u/s (FlowField is the law). The slope term reads the
|
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
|
|
|
|
// downhill direction, which is the force.
|
|
|
|
|
|
public float3 Flow(float3 position) =>
|
|
|
|
|
|
FlowField.At(Settings.Flow, position, FlowScroll, GetForce(position.xz));
|
2026-10-13 20:06:35 +02:00
|
|
|
|
|
Flow: register the flow keywords, one zone-time value for Zone.Flow and the scroll, presenter phases from zone time, folded scroll, tighter parity
- VolumeSampling registers FLOW_GLOBAL / FLOW_SLOPE with GlobalKeyword.Create (the constructor only looks up).
- FlowField.Scroll folds each Tri3D octave's phase modulo 1 in double (float2, [0,1)); Zone.Flow evaluates at Zone.FlowScroll.
- FlowField.Cycle: the nebula's flow cycle and Stardust's particle lifetime phase come from zone time (_FlowCycle, phase uniform), not _Time.y / Time.time.
- FlowFieldParityVerify reaches the keyword-gated flow(), fails on logged errors, tolerance 3e-4; mutants updated.
Not built or run when committed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:43:41 +02:00
|
|
|
|
// The flow's noise scroll at zone time (Zone.Flow evaluates at exactly this); lowered to the shaders' _FlowScroll.
|
|
|
|
|
|
public float2 FlowScroll => FlowField.Scroll(Settings.Flow, _time);
|
|
|
|
|
|
|
|
|
|
|
|
// The phase of a cycle of `period` seconds at zone time, for presenters that animate on the flow.
|
|
|
|
|
|
public float CyclePhase(float period) => FlowField.Cycle(_time, period);
|
2026-10-13 20:06:35 +02:00
|
|
|
|
|
2020-11-29 22:50:12 +00:00
|
|
|
|
public static float PowerPulse(float x, float exponent)
|
|
|
|
|
|
{
|
2020-12-01 00:16:11 +00:00
|
|
|
|
x *= 2;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
x = clamp(x, -1, 1);
|
|
|
|
|
|
return pow((x + 1) * (1 - x), exponent);
|
2020-11-29 22:50:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
|
|
|
|
// The surface normal of the drawn height (potential / K), derived from the force. Display only (ship tilt): the
|
|
|
|
|
|
// sim never reads it as a force.
|
|
|
|
|
|
public float3 GetNormal(float2 pos)
|
2020-11-29 22:50:12 +00:00
|
|
|
|
{
|
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
|
|
|
|
var slope = GetForce(pos) / Settings.HeightExaggeration;
|
|
|
|
|
|
return normalize(new float3(slope.x, 1f, slope.y));
|
2020-11-29 22:50:12 +00:00
|
|
|
|
}
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-12-01 00:16:11 +00:00
|
|
|
|
public class Planet
|
|
|
|
|
|
{
|
2020-12-03 17:32:34 +00:00
|
|
|
|
public Orbit Orbit;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
protected readonly PlanetSettings Settings;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public BodyData BodyData;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
public float BodyRadius;
|
2020-12-01 00:16:11 +00:00
|
|
|
|
|
2026-10-15 16:46:53 +02:00
|
|
|
|
// The well (GravityField): strength c by mass, and the Roche limit, which is both the closest a child may orbit and
|
|
|
|
|
|
// the well's softened core radius R (one curve of mass, never per body). Both are authored PlanetSettings curves.
|
Gravity: one potential per zone owns acceleration and height
GravityField is the law of one well (logarithmic tail, softened core).
Zone.GetForce and GetHeight sum it over the bodies; GetNormal derives from
the force for ship tilt; Flow and Ship read GetForce. Well strength, core
radius and Roche limit are PlanetSettings curves of mass, height
exaggeration K one global. Deletes GravityAcceleration, GravityReferenceRate,
GravityStrength, the PowerPulse well, GetNormal's finite differences and
BodyData.GravityDepthExponent. Settings.asset carries the calibrated curves.
Tests: PotentialOwnerTests, the moon's moon placement, retired keys over every
BodyData type, a body-derived giant sweep. Not yet built or run (verify job
follows); this commit may not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:36:28 +02:00
|
|
|
|
public float WellStrength;
|
|
|
|
|
|
public float RocheLimit;
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public Planet(PlanetSettings settings, BodyData data, Orbit orbit)
|
2020-12-01 00:16:11 +00:00
|
|
|
|
{
|
2025-07-18 00:21:33 +01:00
|
|
|
|
Settings = settings;
|
2020-12-03 17:32:34 +00:00
|
|
|
|
Orbit = orbit;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
BodyData = data;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
CalculateProperties();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void CalculateProperties()
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
BodyRadius = Settings.BodyRadius.Evaluate(BodyData.Mass) * BodyData.BodyRadiusMultiplier;
|
Well law: the band term, WellTree owns every term; the old law, per-body sums and Gauss ring law deleted
GravityField is now the band term U(eps, inner, outer) (C4 smoothstep fade, closed form and a series for tau > 1).
WellTree is built once per Zone from the orbit tree: node terms, reach, support, the pruned Force/Potential walk and
PullAround (rails). Zone.GetHeight/GetForce/PeriodAround delegate to it; WellReference, _wellOn, _strengths,
Strength() and DrawAhead.Well are gone; GravityLowering and the shader template take the flat term list;
HeightExaggeration halves to 2.0491803.
Does not pass yet: Gravity.cginc is not regenerated and PotentialOwnerTests/OrbitsFromFieldTests still spell the old law.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:56:25 +02:00
|
|
|
|
(WellStrength, RocheLimit) = WellTree.Curves(Settings, BodyData.Mass);
|
2020-12-01 00:16:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class GasGiant : Planet
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public GasGiantData GasGiantData;
|
2020-12-01 00:16:11 +00:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public GasGiant(PlanetSettings settings, GasGiantData data, Orbit orbit) : base(settings, data, orbit)
|
2020-12-01 00:16:11 +00:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
GasGiantData = data;
|
2020-12-01 00:16:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-04-12 04:20:52 +01:00
|
|
|
|
public class Sun : GasGiant
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public SunData SunData;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
public float LightRadius;
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public Sun(PlanetSettings settings, SunData data, Orbit orbit) : base(settings, data, orbit)
|
2021-04-12 04:20:52 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
SunData = data;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
CalculateProperties();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public new void CalculateProperties()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.CalculateProperties();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
LightRadius = Settings.LightRadius.Evaluate(BodyData.Mass) * SunData.LightRadiusMultiplier;
|
2021-04-12 04:20:52 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2020-12-01 00:16:11 +00:00
|
|
|
|
|
2020-11-27 22:47:33 +00:00
|
|
|
|
public class AsteroidBelt
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public AsteroidBeltData Data;
|
2022-07-19 18:55:17 +01:00
|
|
|
|
public float Radius { get; }
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
2026-10-15 18:11:27 +02:00
|
|
|
|
// Each asteroid's orbital period, in seconds: derived once at zone build by Zone.PeriodAround (the field at the
|
|
|
|
|
|
// asteroid's own distance from the belt's parent), the only period Pose and BeltTargets divide by.
|
|
|
|
|
|
public float[] Periods { get; }
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public AsteroidBelt(AsteroidBeltData data)
|
2020-11-27 22:47:33 +00:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Data = data;
|
2026-10-15 18:11:27 +02:00
|
|
|
|
Periods = new float[data.Asteroids.Length];
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Radius = data.Asteroids.Max(a => a.Distance);
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
|
2026-10-18 16:28:42 +02:00
|
|
|
|
// What the belt's rail adds to its parent's L_F (WellTree): an annulus reaches its outermost rock, whose period reads the
|
2026-10-21 20:28:35 +02:00
|
|
|
|
// field there; a swarm riding its rail reaches only the rail, whose period every rock shares. The one belt extent rule:
|
|
|
|
|
|
// Zone, ZoneGenerator's clearance tree and the station tests all read it.
|
|
|
|
|
|
public static float Extent(AsteroidBeltData data, float railDistance) =>
|
|
|
|
|
|
data.RidesRail ? railDistance : Math.Max(data.Asteroids.Max(a => a.Distance), railDistance);
|
2026-10-18 16:28:42 +02:00
|
|
|
|
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
// Cut 1 (docs/mining-cut.md): the formula of Zone.cs:269-271 verbatim, `time` in double as `_time` was used.
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
// The only place a chunk pose is computed; no per-tick copy survives it. Cut 2: size is no longer read from
|
|
|
|
|
|
// per-belt storage -- Zone owns wear now, so the caller (Zone.ChunkPose/EvaluateBelt) computes it through
|
|
|
|
|
|
// Zone.ChunkRadius and passes it in.
|
2026-10-15 18:11:27 +02:00
|
|
|
|
public float4 Pose(int index, double time, float2 parentPosition, float size)
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
{
|
|
|
|
|
|
var asteroid = Data.Asteroids[index];
|
|
|
|
|
|
var rot = (float) (time * asteroid.RotationSpeed % (PI * 2));
|
2026-10-15 18:11:27 +02:00
|
|
|
|
var pos = OrbitData.Evaluate((float) frac(time / Periods[index] + asteroid.Phase)) * asteroid.Distance + parentPosition;
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
return float4(pos.x, pos.y, rot, size);
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
// Cut 1's size rule (Zone.cs:259-267), split in Cut 2 into its undamaged and damaged halves now that wear
|
|
|
|
|
|
// lives on Zone instead of here. Neither reads wear directly; the caller (Zone.ChunkRadius) decides which
|
|
|
|
|
|
// applies and supplies the damage.
|
|
|
|
|
|
public float UndamagedSize(int index, PlanetSettings settings) => settings.AsteroidSize.Evaluate(Data.Asteroids[index].Size);
|
|
|
|
|
|
|
|
|
|
|
|
public float DamagedSize(int index, float damage, PlanetSettings settings)
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
{
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
var asteroidHitpoints = settings.AsteroidHitpoints.Evaluate(Data.Asteroids[index].Size);
|
|
|
|
|
|
var remaining = (asteroidHitpoints - damage) / asteroidHitpoints;
|
|
|
|
|
|
return settings.AsteroidSize.Evaluate(remaining * Data.Asteroids[index].Size);
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
}
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cut 2 (docs/mining-cut.md): a chunk is (field, index). "Field" is deliberate, not "belt" -- the operator's
|
|
|
|
|
|
// ruling generalizes chunks to other kinds of debris field later; only AsteroidBelt/AsteroidBeltData, the one
|
|
|
|
|
|
// live field kind, keep their existing names.
|
|
|
|
|
|
public readonly struct ChunkId : IEquatable<ChunkId>
|
|
|
|
|
|
{
|
|
|
|
|
|
public readonly CultRecordKey Field;
|
|
|
|
|
|
public readonly int Index;
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
public ChunkId(CultRecordKey field, int index)
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
{
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
Field = field;
|
|
|
|
|
|
Index = index;
|
Cut 1: belts are evaluated when asked, not simulated every tick
Zone.Update no longer threads a per-belt Task.Run over a double-buffered
Transforms/OrbitPosition pair. AsteroidBelt.Pose/Size are the one place a
chunk pose is computed, a pure function of zone time; Zone.ChunkPose and
Zone.EvaluateBelt read them at the zone's own time. MiningTool,
ResourceScanner and ZoneRenderer (belt matrices and AsteroidBeltUI) all read
through Zone instead of the deleted per-tick buffers; the renderer owns its
own float4[] buffer, filled fresh every frame.
Deleted: System.Threading/.Tasks usings, BeltUpdates, the wait loop, the
per-belt copy+Task.Run, NearestAsteroid (dead/broken reader), the private
UpdateAsteroidTransforms, and AsteroidBelt's four pose fields plus their
allocations. ~65 lines, matching the cut map's estimate.
miningprobe, densest zone of the operator's save (EAC-2265, 809 asteroids,
8 belts), rebuilt against this cut and rerun 10x each side:
before (branch point, 8bd25f6f/e729e6d6):
Zone.Update wall 541-3654 us/tick, CPU 120-391 us/tick (7 clean runs)
3/10 runs crashed: InvalidOperationException from Zone.GetOrbitPosition,
the documented _updatedOrbits/Orbits data race between the main thread's
Clear() and a belt Task.Run
after (this commit):
Zone.Update wall 95-185 us/tick, CPU 99-152 us/tick, in line with the
31-39 us "no belt work" baseline the map recorded -- belts are free per
tick
0/10 races
ChunkPose has no buffer to go stale: ~0.12 u of parent+asteroid motion is
visible across one 17 ms tick instead of being one tick behind
tests/Aetheria.Shared.Tests/MiningCut1Tests.cs (new, 260/260 suite green):
ChunkPoseIsTheOrbitAtZoneTime, ChunkPoseDoesNotDependOnUpdateCadence,
DamagedChunkShrinksAndABrokenOneHasNoSize -- each expected value is the
closed-form orbit/size rule reimplemented in the test, never a call to
AsteroidBelt.Pose/Size.
Negative grep (Zone.cs, "Zone Display", ServerShared/Behaviors) for
Task\.Run|System\.Threading|NewTransforms|\.Transforms\b|BeltUpdates|
NearestAsteroid|\.OrbitPosition\b|NewOrbitPosition: one hit, the commented
line in MineAsteroid that Cut 2 deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 01:27:16 +02:00
|
|
|
|
}
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
|
|
|
|
|
|
public bool Equals(ChunkId other) => Field.Equals(other.Field) && Index == other.Index;
|
|
|
|
|
|
public override bool Equals(object obj) => obj is ChunkId other && Equals(other);
|
|
|
|
|
|
public override int GetHashCode() => (Field.GetHashCode() * 397) ^ Index;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Mining Cut 3: field kinds, chunk visibility, the chunk query, and TargetRef
- FieldKindData, a catalog document (Q11): a kind's reflectivity per schematic
cell and its generation weight. AetheriaStores routes it to the catalog.
- AsteroidBeltData.Kind, key 10 (Q16 B). FieldKinds.Assign is the one pick (a
weighted choice by a stable hash of the belt's key); FieldKinds.Ensure gives
a kindless belt its kind and saves it. ZoneGenerator calls it at generation
and Zone calls it on first load, so an old belt is assigned once by the same
rule and keeps its kind after later catalog edits.
- Zone.ChunkVisibility: Reflector's rule for a chunk -- kind cross-section x
area in schematic cells x the light on it. Stored nowhere; a worn chunk dims,
a broken one or a kindless belt's is dark.
- Zone.ChunksNear: the one chunk query, skipping belts whose annulus cannot
reach. AsteroidBelt gains InnerRadius for that skip.
- TargetRef: an entity, a chunk, or none, with value equality and no ==.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:56:24 +02:00
|
|
|
|
// Mining Cut 3 (docs/mining-cut-refresh.md, Q2 A): what an entity targets -- another entity, a chunk, or nothing.
|
|
|
|
|
|
// One slot holds either kind (Entity.Target, written only by Entity.SetTarget). A chunk never equals an entity.
|
|
|
|
|
|
// There is deliberately no == operator: a comparison against null or an entity must say which side it means.
|
|
|
|
|
|
public readonly struct TargetRef : IEquatable<TargetRef>
|
|
|
|
|
|
{
|
|
|
|
|
|
public readonly Entity Entity;
|
|
|
|
|
|
public readonly ChunkId? Chunk;
|
|
|
|
|
|
|
|
|
|
|
|
public TargetRef(Entity entity)
|
|
|
|
|
|
{
|
|
|
|
|
|
Entity = entity;
|
|
|
|
|
|
Chunk = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public TargetRef(ChunkId chunk)
|
|
|
|
|
|
{
|
|
|
|
|
|
Entity = null;
|
|
|
|
|
|
Chunk = chunk;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static TargetRef None => default;
|
|
|
|
|
|
public bool IsNone => Entity == null && !Chunk.HasValue;
|
|
|
|
|
|
|
|
|
|
|
|
public static implicit operator TargetRef(Entity entity) => new TargetRef(entity);
|
|
|
|
|
|
public static implicit operator TargetRef(ChunkId chunk) => new TargetRef(chunk);
|
|
|
|
|
|
|
|
|
|
|
|
public bool Equals(TargetRef other) => ReferenceEquals(Entity, other.Entity) && Nullable.Equals(Chunk, other.Chunk);
|
|
|
|
|
|
public override bool Equals(object obj) => obj is TargetRef other && Equals(other);
|
|
|
|
|
|
public override int GetHashCode() => Chunk.HasValue ? Chunk.Value.GetHashCode() : Entity?.GetHashCode() ?? 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Cut 2: the dead mining path goes, and chunks get one owner
MiningTool (its Execute range-checked against nothing, since no writer ever
set AsteroidBelt/Asteroid) and Zone.MineAsteroid (mints nothing; the
SimpleCommodity drop was commented out) are deleted, per Q1=A: "Bring back
the drill, indeed." Union tag 26 is retired, not reused. ResourceScanner's
dead survey Execute and its scan-target fields go too; its authored Range/
MinimumDensity/ScanDuration stay pending Q2. PlanetSettings.MiningDifficulty
had one reader, now gone.
Chunk wear (damage, broken-until) moves off AsteroidBelt's three per-belt
dictionaries and onto Zone as the one owner (Q3=A): Zone.Wear is the only
writer, ChunkExists/ChunkRadius are the readers, and respawn is a lazy
comparison against zone time, never a per-tick loop. ChunkId is
(CultRecordKey Field, int Index) -- "Field", not "Belt", because chunks are
meant to generalize past asteroid belts to other debris fields later.
Persisted in ZonePack's new key 6 (List<ChunkWearPack>, nullable so an older
record with no key 6 loads with no wear instead of throwing), pruned of
healed, non-broken entries at pack.
The break threshold stays strictly greater-than, matching today's behaviour:
damage exactly equal to hitpoints does not yet break the chunk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:44 +02:00
|
|
|
|
// Cut 2: wear on one chunk. BrokenUntil is the absolute zone time the chunk respawns at; null means the chunk
|
|
|
|
|
|
// carries damage (or none) but is not currently broken. Respawn is read lazily by comparing to zone time --
|
|
|
|
|
|
// nothing decrements or clears this on a timer.
|
|
|
|
|
|
public struct ChunkWear
|
|
|
|
|
|
{
|
|
|
|
|
|
public float Damage;
|
|
|
|
|
|
public double? BrokenUntil;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class Orbit
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public OrbitData Data { get; }
|
|
|
|
|
|
public float2 Velocity = float2.zero;
|
|
|
|
|
|
public float2 Position = float2.zero;
|
|
|
|
|
|
public float2 PreviousPosition = float2.zero;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
2026-10-15 18:11:27 +02:00
|
|
|
|
// Seconds per turn, written only by the Zone constructor (Zone.PeriodAround); infinite when the orbit has no
|
|
|
|
|
|
// field to circle in.
|
|
|
|
|
|
public float Period { get; internal set; }
|
|
|
|
|
|
|
|
|
|
|
|
public Orbit(OrbitData data)
|
2026-06-14 19:43:48 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Data = data;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|