Stream bounded reactive gameplay state

This commit is contained in:
Meta Vens 2026-07-19 11:17:13 +01:00
commit 68b91b2196
7 changed files with 105 additions and 22 deletions

View file

@ -822,6 +822,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
InputCapabilityPublishesConsumableActionBarState,
InputCapabilityPublishesHeldWeaponGroupLever,
InputCapabilityPublishesNativeTargetAndInteractControls,
ReactiveGameplayPublishesOnlyNewEventFacts,
ReflectorUsesSharedStellarLightFieldWithoutAccumulating,
SensorPingIsActorScopedEnergyGatedAndReconnectable,
TurretControllerAcquiresAimsAndTriggersExactWeapons,
@ -3974,7 +3975,11 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
new AetheriaRuntimeDaemonFrameDocument { FrameId = 12, Run = run },
new AetheriaRuntimeDaemonHealthDocument(),
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
var combat = Flatten(surface.Surface.Root).Single(node => node.Kind == "combat.presentation");
var reactiveSurface = AetheriaRuntimeDaemonGameSurfaceBuilder.BuildReactiveGameplay(
new AetheriaRuntimeDaemonFrameDocument { FrameId = 12, Run = run });
Require(!Flatten(surface.Surface.Root).Any(node => node.Kind == "combat.presentation"),
"the topology surface must not retain dynamic combat presentation authority");
var combat = Flatten(reactiveSurface.Surface.Root).Single(node => node.Kind == "combat.presentation");
var world = Flatten(surface.Surface.Root).Single(node => node.Kind == "world.scene3d");
var aim = Flatten(surface.Surface.Root).Single(node => node.Kind == "aim.presentation");
Require(!string.IsNullOrWhiteSpace(world.Props["lookCommand"]) &&
@ -4107,6 +4112,12 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
$"target={state.LockTargetEntityIndex} progress={state.LockProgress:0.###} shots={run.ShotReceipts.Count} events={string.Join('|', run.GameEvents.Where(value => value.Kind.StartsWith("weapon.lock.", StringComparison.Ordinal)).Select(value => $"{value.Kind}:{value.TargetEntityIndex}:{value.Reason}:{value.AuxiliaryValue:0.###}->{value.ScalarValue:0.###}"))}");
var resetSurface = AetheriaRuntimeDaemonGameSurfaceBuilder.BuildReactiveGameplay(
new AetheriaRuntimeDaemonFrameDocument { FrameId = 22, Run = run });
var resetCombat = Flatten(resetSurface.Surface.Root).Single(node => node.Kind == "combat.presentation");
Require(resetSurface.Version == 22 &&
resetCombat.Props["selectedTargetEntityId"] == "" &&
resetCombat.Props["selectedTargetEntityIndex"] == "-1" &&
resetCombat.Props["targetVisible"] == "false",
"reactive combat presentation must advance with the daemon frame and clear an invalid target");
Require(Flatten(resetSurface.Surface.Root).Any(node => node.Kind == "feedback.event" &&
node.Props["eventKind"] == "weapon.lock.lost" &&
node.Props["targetEntityIndex"] == "2" &&
@ -8146,6 +8157,46 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
"Eve feedback must project authoritative projectile impact identity");
}
private static void ReactiveGameplayPublishesOnlyNewEventFacts()
{
var player = Entity(0, 0, "player");
var run = new AetheriaRuntimeRunCheckpointCommit
{
RunId = "reactive-delta-smoke",
CurrentZoneIndex = 0,
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [player] }],
GameEvents =
[
new AetheriaRuntimeGameEventCommit { EventId = "old-event", Kind = "old", FrameId = 10, SourceEntityIndex = 0 },
new AetheriaRuntimeGameEventCommit { EventId = "new-event", Kind = "new", FrameId = 12, SourceEntityIndex = 0 }
],
ShotReceipts =
[
new AetheriaRuntimeShotReceiptCommit { ShotId = "old-shot", FrameId = 10, SourceEntityIndex = 0 },
new AetheriaRuntimeShotReceiptCommit { ShotId = "new-shot", FrameId = 12, SourceEntityIndex = 0 }
]
};
run.CurrentEntityKey = run.EntityRecordKey(0, player.EntityIndex);
var delta = AetheriaRuntimeDaemonGameSurfaceBuilder.BuildReactiveGameplay(
new AetheriaRuntimeDaemonFrameDocument { FrameId = 12, Run = run },
eventsAfterFrame: 10);
var nodes = Flatten(delta.Surface.Root).ToArray();
Require(nodes.Count(node => node.Kind == "feedback.event") == 1 &&
nodes.Single(node => node.Kind == "feedback.event").Props["eventId"] == "new-event",
"reactive gameplay must publish only event facts newer than the previous delivery frame");
Require(nodes.Count(node => node.Kind == "shot.receipt") == 1 &&
nodes.Single(node => node.Kind == "shot.receipt").Props["shotId"] == "new-shot",
"reactive gameplay must not resend retained shot history in every surface update");
var empty = AetheriaRuntimeDaemonGameSurfaceBuilder.BuildReactiveGameplay(
new AetheriaRuntimeDaemonFrameDocument { FrameId = 13, Run = run },
eventsAfterFrame: 12);
Require(!Flatten(empty.Surface.Root).Any(node =>
node.Kind == "feedback.event" || node.Kind == "shot.receipt"),
"a later reactive update must carry current variables without replaying old event arrays");
}
private static void ProjectileContactCannotKill()
{
var (run, _, target) = Scenario();