Route entity destruction through checkpoints

This commit is contained in:
Meta Vens 2026-06-16 04:47:42 +01:00
commit 729ab0e8d8
5 changed files with 111 additions and 25 deletions

Binary file not shown.

View file

@ -20,6 +20,7 @@ RequireInventoryEntityRenameCommitAuthority(root);
RequireWeaponGroupCommitAuthority(root);
RequireInventoryDoubleClickTransferCommitAuthority(root);
RequireLootPickupCommitAuthority(root);
RequireEntityDestroyedCommitAuthority(root);
RequireTradePurchaseCommitAuthority(root);
RequireInventoryLoadoutRestoreCommitAuthority(root);
RequireDockedCurrentShipCommitAuthority(root);
@ -471,6 +472,8 @@ Console.WriteLine("Hull conductivity authority: inventory UI toggles flow throug
Console.WriteLine("Inventory entity rename authority: UI rename flows through gameplay checkpoint commits");
Console.WriteLine("Weapon group authority: UI assignment flows through gameplay checkpoint commits");
Console.WriteLine("Inventory transfer authority: UI transfer and drag/drop requests flow through gameplay checkpoint commits");
Console.WriteLine("Loot pickup authority: collision pickup requests flow through gameplay checkpoint commits");
Console.WriteLine("Entity destruction authority: hull-death observers flow through gameplay checkpoint commits");
Console.WriteLine("Trade purchase authority: UI buy requests flow through gameplay checkpoint commits");
Console.WriteLine("Inventory loadout restore authority: UI restore requests flow through gameplay checkpoint commits");
Console.WriteLine("Docked current-ship authority: UI selection requests flow through gameplay checkpoint commits");
@ -1162,6 +1165,64 @@ static void RequireLootPickupCommitAuthority(string root)
}
}
static void RequireEntityDestroyedCommitAuthority(string root)
{
var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs");
var actionGameManager = File.Exists(actionGameManagerPath)
? File.ReadAllText(actionGameManagerPath)
: throw new InvalidOperationException("Cannot verify entity destruction authority; ActionGameManager.cs is missing.");
var requiredSymbols = new[]
{
"CommitEntityDestroyed",
"QueueRunCheckpoint(\"entity-destroyed\")",
"entity.Zone.Entities.Remove(entity)",
"zoneRenderer.DropItem("
};
var missingSymbols = requiredSymbols
.Where(symbol => !actionGameManager.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingSymbols.Length > 0)
{
throw new InvalidOperationException(
"Entity destruction no longer has a gameplay-owned checkpoint commit primitive: " +
string.Join(", ", missingSymbols));
}
var entityInstancePath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "EntityInstance.cs");
var entityInstance = File.Exists(entityInstancePath)
? File.ReadAllText(entityInstancePath)
: throw new InvalidOperationException("Cannot verify entity destruction authority; EntityInstance.cs is missing.");
var forbiddenSymbols = new[]
{
"ZoneRenderer.DropItem(",
"entity.Zone.Entities.Remove(entity)",
"Random.onUnitSphere",
"LootDropProbability"
};
var hits = File.ReadLines(entityInstancePath)
.Select((line, index) => new { LineNumber = index + 1, Line = line })
.Where(line => forbiddenSymbols.Any(symbol => line.Line.Contains(symbol, StringComparison.Ordinal)))
.Select(line => $"{Path.GetRelativePath(root, entityInstancePath)}:{line.LineNumber}: {line.Line.Trim()}")
.ToArray();
if (hits.Length > 0)
{
throw new InvalidOperationException(
"EntityInstance still owns direct destruction/drop mutation: " +
string.Join("; ", hits));
}
if (!entityInstance.Contains("ActionGameManager.Instance?.CommitEntityDestroyed", StringComparison.Ordinal))
{
throw new InvalidOperationException("EntityInstance no longer routes destruction through ActionGameManager.");
}
}
static void RequireInventoryLoadoutRestoreCommitAuthority(string root)
{
var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs");

View file

@ -1587,6 +1587,43 @@ public class ActionGameManager : MonoBehaviour
return false;
}
public bool CommitEntityDestroyed(Entity entity, ZoneRenderer zoneRenderer)
{
if (entity?.Zone == null)
return false;
if (zoneRenderer != null)
{
foreach (var gear in entity.Equipment)
{
if (gear == entity.EquippedHull || Random.value >= zoneRenderer.Settings.LootDropProbability)
continue;
zoneRenderer.DropItem(
entity.Position,
Random.onUnitSphere * zoneRenderer.Settings.LootDropVelocity,
gear.EquippableItem);
}
foreach (var cargo in entity.CargoBays)
{
foreach (var item in cargo.Cargo.Keys.ToArray())
{
zoneRenderer.DropItem(
entity.Position,
Random.onUnitSphere * zoneRenderer.Settings.LootDropVelocity,
item);
}
}
}
if (!entity.Zone.Entities.Remove(entity))
return false;
QueueRunCheckpoint("entity-destroyed");
return true;
}
public bool CommitCargoItemTransfer(
EquippedCargoBay origin,
EquippedCargoBay destination,

View file

@ -8,7 +8,6 @@ using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
using float2 = Unity.Mathematics.float2;
using Random = UnityEngine.Random;
public class EntityInstance : MonoBehaviour
{
@ -421,37 +420,18 @@ public class EntityInstance : MonoBehaviour
_subscriptions.Add(Entity.HullDamage.Subscribe(_ =>
{
if (Entity.Hull.Durability < .01f)
if (!_destroyed && Entity.Hull.Durability < .01f)
{
if (!this) return;
_destroyed = true;
foreach (var gear in Entity.Equipment)
{
if (gear != Entity.EquippedHull && Random.value < ZoneRenderer.Settings.LootDropProbability)
{
ZoneRenderer.DropItem(
Entity.Position,
Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity,
gear.EquippableItem);
}
}
if (ActionGameManager.Instance?.CommitEntityDestroyed(entity, ZoneRenderer) != true)
return;
foreach (var cargo in Entity.CargoBays)
{
foreach (var item in cargo.Cargo.Keys)
{
ZoneRenderer.DropItem(
Entity.Position,
Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity,
item);
}
}
_destroyed = true;
if (DestroyEffect != null)
{
var t = Instantiate(DestroyEffect).transform;
t.position = transform.position;
}
entity.Zone.Entities.Remove(entity);
}
}));

View file

@ -1414,6 +1414,12 @@ First Aetheria surfaces to publish:
after a successful commit, but it no longer stores items into cargo bays
directly; gameplay owns the cargo mutation and queues the typed run
checkpoint.
- Done: route entity destruction through
`ActionGameManager.CommitEntityDestroyed`. `EntityInstance` observes hull
death and may spawn the local destruction effect, but gameplay owns
equipment/cargo drop decisions, zone entity removal, and the typed run
checkpoint. Dropped world pickups are still presentation/runtime objects
until the runtime object graph pass gives them typed documents.
- Replace the next concrete uGUI screen with an Eve-owned surface.
- Move the staged packages into the Eve repo once its worktree is clean, then
import them back into Aetheria from Eve instead of carrying a local copy.
@ -1749,7 +1755,9 @@ instantiation, credits, dock assignment, current entity, and checkpoint. Docked
current-ship selection follows the same rule: UI requests selection, gameplay
owns `CurrentEntity`, `DockingBay.DockedShip`, and checkpoint. Loot pickup
follows the same rule: collision code reports the pickup opportunity, gameplay
owns cargo storage and checkpoint. Any
owns cargo storage and checkpoint. Entity destruction follows the same rule:
instance code observes death, gameplay owns drop decisions, zone removal, and
checkpoint. Any
predicate that still needs legacy DTO objects must earn that dependency by
using behavior objects or simulation-only methods that typed facets do not yet
expose.