Eliminate repeated behavior graph rebuilds

This commit is contained in:
Meta Vens 2026-07-19 10:24:57 +01:00
commit bd89cb4c85
6 changed files with 143 additions and 7 deletions

View file

@ -37,7 +37,17 @@ namespace GameCult.Aetheria.State.Verse
int targetEquipmentIndex,
AetheriaRuntimeBehaviorPayload targetBehavior,
int targetFieldKey,
double baseline)
double baseline) =>
Apply(entity, catalog, targetEquipmentIndex, targetBehavior, targetFieldKey, baseline, null);
internal static double Apply(
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeCatalogSnapshot catalog,
int targetEquipmentIndex,
AetheriaRuntimeBehaviorPayload targetBehavior,
int targetFieldKey,
double baseline,
IReadOnlyList<AetheriaRuntimeEquippedBehavior>? equippedModifiers)
{
var targetStatName = StatName(targetBehavior?.Kind, targetFieldKey);
if (entity == null || catalog == null || targetEquipmentIndex < 0 ||
@ -46,7 +56,7 @@ namespace GameCult.Aetheria.State.Verse
double multiplier = 1;
double constant = 0;
foreach (var modifier in ActiveEquippedModifiers(entity, catalog)
foreach (var modifier in ActiveEquippedModifiers(entity, catalog, equippedModifiers)
.Concat(ActiveConsumableModifiers(entity, catalog)))
{
if (!Targets(modifier.Payload, entity, catalog, targetEquipmentIndex, targetBehavior, targetStatName))
@ -360,9 +370,11 @@ namespace GameCult.Aetheria.State.Verse
private static IEnumerable<ModifierValue> ActiveEquippedModifiers(
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeCatalogSnapshot catalog)
AetheriaRuntimeCatalogSnapshot catalog,
IReadOnlyList<AetheriaRuntimeEquippedBehavior>? equippedModifiers = null)
{
foreach (var behavior in AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, "StatModifier"))
foreach (var behavior in equippedModifiers ??
AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, "StatModifier"))
{
if (!behavior.State.StatModifierApplied)
continue;

View file

@ -18,6 +18,8 @@ namespace GameCult.Aetheria.State.Verse
return;
var existing = entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>();
if (EquipmentBehaviorStatesMatch(existing, entity.Equipment, catalog))
return;
var projected = CreateEquipmentBehaviorStates(entity.Equipment, catalog);
if (projected.Length == 0)
{
@ -95,5 +97,37 @@ namespace GameCult.Aetheria.State.Verse
state.BehaviorIndex.ToString(System.Globalization.CultureInfo.InvariantCulture),
state.BehaviorKind ?? "");
}
private static bool EquipmentBehaviorStatesMatch(
IReadOnlyList<AetheriaRuntimeBehaviorStateCommit> existing,
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? equipment,
AetheriaRuntimeCatalogSnapshot? catalog)
{
catalog ??= new AetheriaRuntimeCatalogSnapshot();
var matched = 0;
var slots = equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
for (var equipmentIndex = 0; equipmentIndex < slots.Count; equipmentIndex++)
{
var item = slots[equipmentIndex]?.Item;
var payloads = catalog.FindItem(item?.ItemKey ?? "")?.BehaviorPayloads ??
Array.Empty<AetheriaRuntimeBehaviorPayload>();
for (var behaviorIndex = 0; behaviorIndex < payloads.Count; behaviorIndex++)
{
var payload = payloads[behaviorIndex];
if (payload == null || string.IsNullOrWhiteSpace(payload.Kind))
continue;
matched++;
if (!existing.Any(state => state != null &&
string.Equals(state.OwnerKind, EquipmentOwnerKind, StringComparison.Ordinal) &&
state.OwnerIndex == equipmentIndex &&
state.BehaviorIndex == behaviorIndex &&
string.Equals(state.BehaviorKind, payload.Kind, StringComparison.Ordinal)))
return false;
}
}
return existing.Count(state => state != null &&
string.Equals(state.OwnerKind, EquipmentOwnerKind, StringComparison.Ordinal)) == matched;
}
}
}

View file

@ -36,10 +36,15 @@ namespace GameCult.Aetheria.State.Verse
Environment.GetEnvironmentVariable("AETHERIA_TRACE_SIMULATION_PHASES"),
"1",
StringComparison.Ordinal);
var traceSimulationPhaseThreshold = int.TryParse(
Environment.GetEnvironmentVariable("AETHERIA_TRACE_SIMULATION_PHASE_THRESHOLD_MS"),
out var configuredTraceThreshold)
? Math.Max(0, configuredTraceThreshold)
: 20;
var phase = Stopwatch.StartNew();
void TracePhase(string name)
{
if (traceSimulationPhases && phase.ElapsedMilliseconds >= 20)
if (traceSimulationPhases && phase.ElapsedMilliseconds >= traceSimulationPhaseThreshold)
Console.WriteLine($"Aetheria simulation phase {name} took {phase.ElapsedMilliseconds}ms.");
phase.Restart();
}
@ -597,9 +602,13 @@ namespace GameCult.Aetheria.State.Verse
RefreshShieldProjection(entity, catalog);
foreach (var attacker in combatEntities)
{
var requestedWeapons = ResolveWeaponTriggers(zone, attacker, entities, intents, catalog);
var operationalBehaviors = AetheriaRuntimeEquippedBehaviorQueries
.FindAllOperational(attacker, catalog);
if (!operationalBehaviors.Any(behavior =>
AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(
behavior.Payload.Kind, "Weapon")))
continue;
var requestedWeapons = ResolveWeaponTriggers(zone, attacker, entities, intents, catalog);
StepDeployableWeapons(run, zone, attacker, operationalBehaviors, requestedWeapons,
deltaSeconds, settings, catalog, frameId);
StepChargedWeapons(run, zone, attacker, byIndex, requestedWeapons, deltaSeconds,

View file

@ -29,6 +29,8 @@ namespace GameCult.Aetheria.State.Verse
public sealed class AetheriaRuntimeEquippedBehavior
{
private IReadOnlyList<AetheriaRuntimeEquippedBehavior>? modifierBehaviors;
public AetheriaRuntimeEquippedBehavior(
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeCatalogSnapshot catalog,
@ -61,6 +63,9 @@ namespace GameCult.Aetheria.State.Verse
public AetheriaRuntimeBehaviorPayload Payload { get; }
public AetheriaRuntimeBehaviorStateCommit State { get; }
internal void BindModifierBehaviors(IReadOnlyList<AetheriaRuntimeEquippedBehavior> value) =>
modifierBehaviors = value ?? Array.Empty<AetheriaRuntimeEquippedBehavior>();
public double EvaluateStat(int fieldKey, double thermalPerformance = 1.0)
{
var field = (Payload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
@ -70,7 +75,7 @@ namespace GameCult.Aetheria.State.Verse
Item,
Math.Max(0, Math.Min(1, thermalPerformance)));
return AetheriaRuntimeBehaviorSimulation.Apply(
Entity, Catalog, EquipmentIndex, Payload, fieldKey, baseline);
Entity, Catalog, EquipmentIndex, Payload, fieldKey, baseline, modifierBehaviors);
}
}
@ -131,6 +136,18 @@ namespace GameCult.Aetheria.State.Verse
state));
}
}
var equipmentStates = (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
.Where(state => state != null)
.ToDictionary(state => state.EquipmentIndex);
var modifierBehaviors = found
.Where(value => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(
value.Payload.Kind, "StatModifier"))
.Where(value => value.Item.Enabled && value.Item.Durability > 0.01 &&
(!equipmentStates.TryGetValue(value.EquipmentIndex, out var state) || state.Online))
.ToArray();
foreach (var behavior in found)
behavior.BindModifierBehaviors(modifierBehaviors);
return found;
}