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>();
|
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.
|
|
|
|
|
|
internal 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-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-12 04:00:08 +02:00
|
|
|
|
// The death ordinal of the loot dice: the nth death in this zone rolls on SimulationDice.For(CombatSeed, LootStream, n).
|
|
|
|
|
|
private uint _deaths;
|
|
|
|
|
|
|
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;
|
2026-09-19 18:34:04 +02:00
|
|
|
|
CombatSeed = galaxyZone?.Name.StableHash() ?? 1337u;
|
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-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
|
|
|
|
{
|
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.Add(orbit.Key, new Orbit(Settings, 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);
|
2026-10-03 15:02:51 +02:00
|
|
|
|
Targets.Add(new BeltTargets(this, body.Key, belt, settings, _time));
|
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
|
|
|
|
|
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-08-10 14:57:50 +01:00
|
|
|
|
foreach (var entityPack in pack.Entities)
|
2021-03-13 03:23:58 +00:00
|
|
|
|
{
|
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});
|
|
|
|
|
|
// Generation placement, not admission: a ship packed at the origin is scattered across the zone.
|
|
|
|
|
|
if (ship is {IsPlayerShip: false} && lengthsq(ship.Position) < 1)
|
|
|
|
|
|
ship.Position = _itemManager.Random.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.
|
2026-09-30 12:32:59 +02:00
|
|
|
|
public void Admit(Entity entity, bool piloted)
|
|
|
|
|
|
{
|
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();
|
|
|
|
|
|
if (piloted) Agents.Add(CreateAgent((Ship) entity));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-13 02:13:43 +02:00
|
|
|
|
// The one removal from this zone (death, dock, jump): membership and its death watch leave together. False, and
|
|
|
|
|
|
// nothing changes, when the entity is not a member. The caller deactivates; Remove does not.
|
|
|
|
|
|
public bool Remove(Entity entity)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_entities.Remove(entity)) return false;
|
|
|
|
|
|
_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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-03-18 00:36:34 +00:00
|
|
|
|
private Agent CreateAgent(Ship ship)
|
|
|
|
|
|
{
|
|
|
|
|
|
var agent = new Minion(ship);
|
|
|
|
|
|
var task = new PatrolOrbitsTask();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
task.Circuit = Orbits.OrderBy(_ => _itemManager.Random.NextFloat()).Take(4).Select(x => x.Key).ToArray();
|
2021-03-18 00:36:34 +00:00
|
|
|
|
agent.Task = task;
|
|
|
|
|
|
return agent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public void AddOrbit(OrbitData orbit)
|
2026-06-16 15:54:18 +01: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
|
|
|
|
Orbits.Add(_itemManager.ItemData.RefOf(orbit).Key, new Orbit(Settings, orbit));
|
2026-06-16 15:54:18 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
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-08-10 14:57:50 +01:00
|
|
|
|
foreach (var orbit in Orbits)
|
2020-11-27 22:47:33 +00:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
orbit.Value.PreviousPosition = orbit.Value.Position;
|
|
|
|
|
|
orbit.Value.Position = GetOrbitPosition(orbit.Key);
|
|
|
|
|
|
orbit.Value.Velocity = (orbit.Value.Position - orbit.Value.PreviousPosition) / deltaTime;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2021-03-09 02:26:58 +00:00
|
|
|
|
foreach(var agent in Agents)
|
|
|
|
|
|
agent.Update(deltaTime);
|
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-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;
|
|
|
|
|
|
var random = SimulationDice.For(CombatSeed, SimulationDice.LootStream, ++_deaths);
|
|
|
|
|
|
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-08-10 14:57:50 +01:00
|
|
|
|
var orbit = Orbits[orbitID];
|
|
|
|
|
|
float2 pos = float2.zero;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
if (orbit.Period > .01f)
|
2020-11-27 22:47:33 +00:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var phase = (float) frac(_time / orbit.Period);
|
|
|
|
|
|
pos = OrbitData.Evaluate(frac(phase + orbit.Data.Phase)) * orbit.Data.Distance;
|
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
|
|
|
|
|
2020-11-27 22:47:33 +00:00
|
|
|
|
if (float.IsNaN(pos.x))
|
|
|
|
|
|
{
|
2020-12-01 00:16:11 +00:00
|
|
|
|
//_context.Log("Orbit position is NaN, something went very wrong!");
|
2026-08-10 14:57:50 +01:00
|
|
|
|
pos = float2.zero;
|
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
|
|
|
|
var parentPosition = !Orbits[orbitID].Data.Parent.IsSet()
|
|
|
|
|
|
? Orbits[orbitID].Data.FixedPosition :
|
|
|
|
|
|
GetOrbitPosition(orbit.Data.Parent.Key);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Orbits[orbitID].Position = parentPosition + pos;
|
|
|
|
|
|
_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
|
|
|
|
}
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
public float4 ChunkPose(CultRecordKey belt, int index)
|
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;
|
|
|
|
|
|
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
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));
|
|
|
|
|
|
return AsteroidBelts[belt].Pose(index, _time, parentPosition, Settings, 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
public void EvaluateBelt(CultRecordKey belt, Span<float4> into)
|
|
|
|
|
|
{
|
|
|
|
|
|
var beltData = Planets[belt] as AsteroidBeltData;
|
|
|
|
|
|
var parentPosition = GetOrbitPosition(Orbits[beltData.Orbit.Key].Data.Parent.Key);
|
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++)
|
|
|
|
|
|
into[i] = beltInstance.Pose(i, _time, parentPosition, Settings, ChunkRadius(new ChunkId(belt, i)));
|
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 OrbitData CreateOrbit(CultRecordKey parent, float2 position)
|
2020-11-27 22:47:33 +00:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var parentPosition = GetOrbitPosition(parent);
|
|
|
|
|
|
var delta = position - parentPosition;
|
|
|
|
|
|
var distance = length(delta);
|
|
|
|
|
|
var period = Settings.OrbitPeriod.Evaluate(distance);
|
|
|
|
|
|
var phase = atan2(delta.y, delta.x) / (PI * 2);
|
|
|
|
|
|
var currentPhase = frac(_time / period);
|
|
|
|
|
|
var storedPhase = (float) frac(phase - currentPhase);
|
|
|
|
|
|
|
|
|
|
|
|
var orbit = new OrbitData
|
|
|
|
|
|
{
|
|
|
|
|
|
Distance = distance,
|
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
|
|
|
|
Parent = new CultRecordRef<OrbitData>(parent),
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Phase = storedPhase
|
|
|
|
|
|
};
|
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.Add(_itemManager.ItemData.Upsert(orbit).Key, new Orbit(Settings, orbit));
|
2026-08-10 14:57:50 +01:00
|
|
|
|
return orbit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2020-11-29 22:50:12 +00:00
|
|
|
|
public float GetHeight(float2 position)
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var result = -PowerPulse(length(position)/(Pack.Radius*2), Settings.ZoneDepthExponent) * Settings.ZoneDepth;
|
2020-12-01 00:16:11 +00:00
|
|
|
|
foreach (var body in PlanetInstances.Values)
|
2020-11-29 22:50:12 +00:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var p = position - body.Orbit.Position; //GetOrbitPosition(body.BodyData.Orbit)
|
|
|
|
|
|
var distSqr = lengthsq(p);
|
2025-07-18 00:21:33 +01:00
|
|
|
|
var gravityRadius = body.GravityWellRadius;
|
2020-12-22 21:32:37 +00:00
|
|
|
|
if (distSqr < gravityRadius*gravityRadius)
|
2020-11-29 22:50:12 +00:00
|
|
|
|
{
|
2025-07-18 00:21:33 +01:00
|
|
|
|
var depth = body.GravityWellDepth;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
result -= PowerPulse(sqrt(distSqr) / gravityRadius, body.BodyData.GravityDepthExponent) * depth;
|
2020-11-29 22:50:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-12-01 00:16:11 +00:00
|
|
|
|
if (body is GasGiant gas)
|
2020-11-29 22:50:12 +00:00
|
|
|
|
{
|
2025-07-18 00:21:33 +01:00
|
|
|
|
var waveRadius = gas.GravityWavesRadius;
|
2020-12-22 21:32:37 +00:00
|
|
|
|
if(distSqr < waveRadius*waveRadius)
|
2020-11-29 22:50:12 +00:00
|
|
|
|
{
|
2025-07-18 00:21:33 +01:00
|
|
|
|
var depth = gas.GravityWavesDepth;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
var frequency = Settings.WaveFrequency.Evaluate(body.BodyData.Mass);
|
2025-07-18 00:21:33 +01:00
|
|
|
|
var speed = gas.GravityWavesSpeed;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
result -= RadialWaves(sqrt(distSqr) / waveRadius, 8, 1.25f, frequency, (float) (_time * speed)) * depth;
|
2020-11-29 22:50:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-10-07 17:32:53 +02:00
|
|
|
|
// The one law of how a gravity well pushes anything: ships, floating items and mines all read it. The shape is
|
|
|
|
|
|
// the one ships have always felt, normalize(f) * strength * (1 / (1 - |f|^2) - 1), gated off below |f|^2 = .001.
|
|
|
|
|
|
// It is an acceleration, per second: the legacy law added that amount once per 60 fps frame with no dt, so
|
|
|
|
|
|
// GravityReferenceRate (60) restates it and `acceleration * dt` gives the legacy push at 60 fps.
|
|
|
|
|
|
public const float GravityReferenceRate = 60f;
|
|
|
|
|
|
|
|
|
|
|
|
public static float2 GravityAcceleration(float2 normalXz, float strength)
|
|
|
|
|
|
{
|
|
|
|
|
|
var magnitude = lengthsq(normalXz);
|
|
|
|
|
|
if (magnitude <= .001f) return float2.zero;
|
|
|
|
|
|
return normalize(normalXz) * strength * (1 / (1 - magnitude) - 1) * GravityReferenceRate;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The acceleration the well exerts at `position`.
|
2020-11-29 22:50:12 +00:00
|
|
|
|
public float2 GetForce(float2 position)
|
|
|
|
|
|
{
|
|
|
|
|
|
var normal = GetNormal(position);
|
2026-10-07 17:32:53 +02:00
|
|
|
|
return GravityAcceleration(new float2(normal.x, normal.z), Settings.GravityStrength);
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static float RadialWaves(float x, float maskExponent, float sineExponent, float frequency, float phase)
|
|
|
|
|
|
{
|
2020-12-22 21:32:37 +00:00
|
|
|
|
//x *= 2;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
return PowerPulse(x, maskExponent) * cos(pow(x*2, sineExponent) * frequency + phase);
|
2020-11-29 22:50:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-12-22 03:13:43 +00:00
|
|
|
|
public float3 GetNormal(float2 pos, float step = .1f, float mul = 1)
|
2020-11-29 22:50:12 +00:00
|
|
|
|
{
|
|
|
|
|
|
float hL = GetHeight(new float2(pos.x - step, pos.y)) * mul;
|
|
|
|
|
|
float hR = GetHeight(new float2(pos.x + step, pos.y)) * mul;
|
|
|
|
|
|
float hD = GetHeight(new float2(pos.x, pos.y - step)) * mul;
|
|
|
|
|
|
float hU = GetHeight(new float2(pos.x, pos.y + step)) * mul;
|
|
|
|
|
|
|
|
|
|
|
|
// Deduce terrain normal
|
2020-12-22 03:13:43 +00:00
|
|
|
|
float3 normal = new float3((hL - hR), (hD - hU), step*2);
|
2026-08-10 14:57:50 +01:00
|
|
|
|
return normalize(normal).xzy;
|
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 GravityWellDepth;
|
|
|
|
|
|
public float GravityWellRadius;
|
|
|
|
|
|
public float BodyRadius;
|
2020-12-01 00:16:11 +00:00
|
|
|
|
|
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;
|
|
|
|
|
|
GravityWellRadius = Settings.GravityRadius.Evaluate(BodyData.Mass) * BodyData.GravityRadiusMultiplier;
|
|
|
|
|
|
GravityWellDepth = Settings.GravityDepth.Evaluate(BodyData.Mass) * BodyData.GravityDepthMultiplier;
|
2020-12-01 00:16:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class GasGiant : Planet
|
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public GasGiantData GasGiantData;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
public float GravityWavesDepth;
|
|
|
|
|
|
public float GravityWavesRadius;
|
|
|
|
|
|
public float GravityWavesSpeed;
|
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;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
CalculateProperties();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public new void CalculateProperties()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.CalculateProperties();
|
2026-08-10 14:57:50 +01:00
|
|
|
|
GravityWavesDepth = Settings.WaveDepth.Evaluate(BodyData.Mass) * GasGiantData.WaveDepthMultiplier;
|
|
|
|
|
|
GravityWavesRadius = Settings.WaveRadius.Evaluate(BodyData.Mass) * GasGiantData.WaveRadiusMultiplier;
|
|
|
|
|
|
GravityWavesSpeed = Settings.WaveSpeed.Evaluate(BodyData.Mass) * GasGiantData.WaveSpeedMultiplier;
|
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-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;
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
|
public float4 Pose(int index, double time, float2 parentPosition, PlanetSettings settings, 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));
|
|
|
|
|
|
var pos = OrbitData.Evaluate((float) frac(time / settings.OrbitPeriod.Evaluate(asteroid.Distance) +
|
|
|
|
|
|
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;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
public float Period;
|
2020-11-27 22:47:33 +00:00
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
|
public Orbit(PlanetSettings settings, OrbitData data)
|
2026-06-14 19:43:48 +01:00
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
|
Data = data;
|
2025-07-18 00:21:33 +01:00
|
|
|
|
Period = settings.OrbitPeriod.Evaluate(data.Distance);
|
2020-11-27 22:47:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|