diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs index a96f60345..bc5a2026d 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; #nullable enable @@ -31,6 +32,18 @@ namespace GameCult.Aetheria.State.Verse if (worldPhysics == null) throw new ArgumentNullException(nameof(worldPhysics)); + var traceSimulationPhases = string.Equals( + Environment.GetEnvironmentVariable("AETHERIA_TRACE_SIMULATION_PHASES"), + "1", + StringComparison.Ordinal); + var phase = Stopwatch.StartNew(); + void TracePhase(string name) + { + if (traceSimulationPhases && phase.ElapsedMilliseconds >= 20) + Console.WriteLine($"Aetheria simulation phase {name} took {phase.ElapsedMilliseconds}ms."); + phase.Restart(); + } + StepWormholeTransitions( run, intents == null @@ -46,6 +59,7 @@ namespace GameCult.Aetheria.State.Verse .Where(zone => zone != null) .Select(zone => zone.ZoneIndex) .ToArray()); + TracePhase("world-retention"); foreach (var zone in run.Zones ?? Array.Empty()) { @@ -93,12 +107,16 @@ namespace GameCult.Aetheria.State.Verse settings.TorqueFloor, settings.TorqueMultiplier); StepTractorPower(entities, deltaSeconds); + TracePhase($"zone-{zone.ZoneIndex}-pre-physics"); var worldStep = StepWorldPhysics( run.RunId, frameId, simulationStepIndex, zone, entities, deltaSeconds, worldPhysics); + TracePhase($"zone-{zone.ZoneIndex}-ymir"); ProjectEntityTerrainHeights(zone, entities, catalog, simulationTimeSeconds); ResolvePickupProximity(run, zone, entities, catalog, frameId); + TracePhase($"zone-{zone.ZoneIndex}-terrain-pickup"); StepCombat(run, zone, entities, intents, deltaSeconds, settings, worldPhysics, catalog, frameId, simulationTimeSeconds, simulationStepIndex); + TracePhase($"zone-{zone.ZoneIndex}-combat"); AetheriaRuntimeVisibilitySimulation.StepZone(zone, entities, catalog, renderSettings); var activeRenderSettings = renderSettings ?? AetheriaRuntimeDaemonRenderSettings.AetheriaDefault; AetheriaRuntimeSensorSimulation.StepZone( @@ -114,7 +132,9 @@ namespace GameCult.Aetheria.State.Verse settings.SecureAreaRadiusMultiplier); AetheriaRuntimeMiningSimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds); AetheriaRuntimeSurveySimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds); + TracePhase($"zone-{zone.ZoneIndex}-sensors-resources"); FinalizeBehaviorChainsAndThermal(run, zone, entities, catalog, deltaSeconds, settings, frameId); + TracePhase($"zone-{zone.ZoneIndex}-thermal-finalize"); } } @@ -578,12 +598,15 @@ namespace GameCult.Aetheria.State.Verse foreach (var attacker in combatEntities) { var requestedWeapons = ResolveWeaponTriggers(zone, attacker, entities, intents, catalog); - StepDeployableWeapons(run, zone, attacker, requestedWeapons, deltaSeconds, settings, catalog, frameId); + var operationalBehaviors = AetheriaRuntimeEquippedBehaviorQueries + .FindAllOperational(attacker, catalog); + StepDeployableWeapons(run, zone, attacker, operationalBehaviors, requestedWeapons, + deltaSeconds, settings, catalog, frameId); StepChargedWeapons(run, zone, attacker, byIndex, requestedWeapons, deltaSeconds, - settings, catalog, frameId); + operationalBehaviors, settings, catalog, frameId); StepConstantWeapons(run, zone, byIndex, attacker, requestedWeapons, deltaSeconds, - settings, catalog, frameId); - var weapons = ResolveWeapons(attacker, catalog, settings); + operationalBehaviors, settings, catalog, frameId); + var weapons = ResolveWeapons(attacker, operationalBehaviors, catalog, settings); foreach (var weapon in weapons) { var autoTriggerReady = @@ -726,13 +749,15 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeRunCheckpointCommit run, AetheriaRuntimeZoneSnapshotCommit zone, AetheriaRuntimeEntitySnapshotCommit attacker, + IReadOnlyList operationalBehaviors, WeaponTriggerSet requestedWeapons, double deltaSeconds, AetheriaRuntimeDaemonSimulationSettings settings, AetheriaRuntimeCatalogSnapshot? catalog, long frameId) { - foreach (var deployable in ResolveDeployableWeapons(attacker, requestedWeapons, catalog, settings)) + foreach (var deployable in ResolveDeployableWeapons( + attacker, operationalBehaviors, requestedWeapons, settings)) { var weapon = deployable.Weapon; weapon.State.Firing = false; @@ -882,11 +907,13 @@ namespace GameCult.Aetheria.State.Verse IReadOnlyDictionary byIndex, WeaponTriggerSet requestedWeapons, double deltaSeconds, + IReadOnlyList operationalBehaviors, AetheriaRuntimeDaemonSimulationSettings settings, AetheriaRuntimeCatalogSnapshot? catalog, long frameId) { - foreach (var weapon in ResolveChargedWeapons(attacker, requestedWeapons, catalog, settings)) + foreach (var weapon in ResolveChargedWeapons( + attacker, operationalBehaviors, requestedWeapons, settings)) { var state = weapon.Base.State; state.Firing = false; @@ -1075,11 +1102,13 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeEntitySnapshotCommit attacker, WeaponTriggerSet requestedWeapons, double deltaSeconds, + IReadOnlyList operationalBehaviors, AetheriaRuntimeDaemonSimulationSettings settings, AetheriaRuntimeCatalogSnapshot? catalog, long frameId) { - foreach (var weapon in ResolveConstantWeapons(attacker, requestedWeapons, catalog, settings)) + foreach (var weapon in ResolveConstantWeapons( + attacker, operationalBehaviors, requestedWeapons, settings)) { if (weapon.State.Reloading) { @@ -1515,11 +1544,13 @@ namespace GameCult.Aetheria.State.Verse private static IReadOnlyList ResolveConstantWeapons( AetheriaRuntimeEntitySnapshotCommit entity, + IReadOnlyList operationalBehaviors, WeaponTriggerSet requestedWeapons, - AetheriaRuntimeCatalogSnapshot? catalog, AetheriaRuntimeDaemonSimulationSettings settings) { - return AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, AetheriaRuntimeBehaviorKinds.ConstantWeapon) + return operationalBehaviors + .Where(behavior => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant( + behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.ConstantWeapon)) .Select(behavior => { var magazineSize = Math.Max(0, (int)Math.Round(ReadNumber(behavior.Payload, 13))); @@ -1546,11 +1577,13 @@ namespace GameCult.Aetheria.State.Verse private static IReadOnlyList ResolveDeployableWeapons( AetheriaRuntimeEntitySnapshotCommit entity, + IReadOnlyList operationalBehaviors, WeaponTriggerSet requestedWeapons, - AetheriaRuntimeCatalogSnapshot? catalog, AetheriaRuntimeDaemonSimulationSettings settings) { - return AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, AetheriaRuntimeBehaviorKinds.DeployableWeapon) + return operationalBehaviors + .Where(behavior => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant( + behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.DeployableWeapon)) .Where(behavior => requestedWeapons.ContainsPulse(behavior.EquipmentIndex, behavior.BehaviorIndex)) .Select(behavior => new ResolvedDeployableWeapon( ResolveAuthoredWeapon(entity, behavior, settings), @@ -1910,10 +1943,14 @@ namespace GameCult.Aetheria.State.Verse } private static IReadOnlyList ResolveChargedWeapons( - AetheriaRuntimeEntitySnapshotCommit entity, WeaponTriggerSet requestedWeapons, - AetheriaRuntimeCatalogSnapshot? catalog, AetheriaRuntimeDaemonSimulationSettings settings) + AetheriaRuntimeEntitySnapshotCommit entity, + IReadOnlyList operationalBehaviors, + WeaponTriggerSet requestedWeapons, + AetheriaRuntimeDaemonSimulationSettings settings) { - return AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, AetheriaRuntimeBehaviorKinds.ChargedWeapon) + return operationalBehaviors + .Where(behavior => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant( + behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.ChargedWeapon)) .Where(value => string.Equals(value.Payload.Kind, AetheriaRuntimeBehaviorKinds.ChargedWeapon, StringComparison.Ordinal)) .Select(value => new ResolvedChargedWeapon( ResolveAuthoredWeapon(entity, value, settings), value.Item, @@ -1929,10 +1966,13 @@ namespace GameCult.Aetheria.State.Verse private static IReadOnlyList ResolveWeapons( AetheriaRuntimeEntitySnapshotCommit entity, + IReadOnlyList operationalBehaviors, AetheriaRuntimeCatalogSnapshot? catalog, AetheriaRuntimeDaemonSimulationSettings settings) { - var authored = AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, AetheriaRuntimeBehaviorKinds.InstantWeapon) + var authored = operationalBehaviors + .Where(behavior => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant( + behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.InstantWeapon)) .Where(behavior => !string.Equals(behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.ChargedWeapon, StringComparison.Ordinal)) .Where(behavior => !string.Equals(behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.DeployableWeapon, StringComparison.Ordinal)) .Select(behavior => ResolveAuthoredWeapon(entity, behavior, settings)) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedBehaviorQueries.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedBehaviorQueries.cs index 358b49e70..6fa57e4ef 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedBehaviorQueries.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeEquippedBehaviorQueries.cs @@ -81,7 +81,20 @@ namespace GameCult.Aetheria.State.Verse AetheriaRuntimeCatalogSnapshot? catalog, string behaviorKind) { - if (entity == null || catalog == null || string.IsNullOrWhiteSpace(behaviorKind)) + if (string.IsNullOrWhiteSpace(behaviorKind)) + return Array.Empty(); + + return FindAll(entity, catalog) + .Where(value => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant( + value.Payload.Kind, behaviorKind)) + .ToArray(); + } + + public static IReadOnlyList FindAll( + AetheriaRuntimeEntitySnapshotCommit? entity, + AetheriaRuntimeCatalogSnapshot? catalog) + { + if (entity == null || catalog == null) return Array.Empty(); AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(entity, catalog); @@ -102,7 +115,6 @@ namespace GameCult.Aetheria.State.Verse { var payload = payloads[behaviorIndex]; if (payload == null || - !AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(payload.Kind, behaviorKind) || !states.TryGetValue((equipmentIndex, behaviorIndex), out var state)) { continue; @@ -139,6 +151,22 @@ namespace GameCult.Aetheria.State.Verse .ToArray(); } + public static IReadOnlyList FindAllOperational( + AetheriaRuntimeEntitySnapshotCommit? entity, + AetheriaRuntimeCatalogSnapshot? catalog) + { + if (entity == null) + return Array.Empty(); + + var equipmentStates = (entity.EquipmentStates ?? Array.Empty()) + .Where(state => state != null) + .ToDictionary(state => state.EquipmentIndex); + return FindAll(entity, catalog) + .Where(value => value.Item.Enabled && value.Item.Durability > 0.01 && + (!equipmentStates.TryGetValue(value.EquipmentIndex, out var state) || state.Online)) + .ToArray(); + } + public static IReadOnlyList FindExecuting( AetheriaRuntimeEntitySnapshotCommit? entity, AetheriaRuntimeCatalogSnapshot? catalog, diff --git a/docs/aetheria-current-codebase-model.md b/docs/aetheria-current-codebase-model.md index 01dc33798..e8888619d 100644 --- a/docs/aetheria-current-codebase-model.md +++ b/docs/aetheria-current-codebase-model.md @@ -360,6 +360,14 @@ Thermotoggle target temperature is also daemon-owned behavior state. Authored data seeds it, adjustable commands mutate it, and its high/low-pass predicate gates the same ordered chain. +Within one combat step, authoritative equipment, online state, durability, and +catalog payloads are read once into a tick-local operational behavior set. +Deployable, charged, constant, and instant weapon projections derive from that +same set. It is not persisted and cannot outlive the step; mutations still land +on the referenced authoritative item and behavior state. This prevents each +weapon family from independently rebuilding the same equipment graph without +introducing a cache owner beside the entity snapshot. + Migration target: - blueprint/catalog rows define base stat ids and default values;