Gate specialized behavior chain tails
This commit is contained in:
parent
020f3aec92
commit
b125f047b4
8 changed files with 294 additions and 107 deletions
|
|
@ -197,6 +197,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
MultipleActorsUseTheSameMovementLever,
|
||||
DirectionalThrustersOwnOrdinaryFlightFeedback,
|
||||
UnpoweredThrustersCannotMoveOrAdvertisePlume,
|
||||
SpecializedBehaviorSuccessGatesTrailingWear,
|
||||
RareAetherDriveSpoolsOnlyOnModifiedHullFixture,
|
||||
LookDirectionRejectsInvalidVectorsWithoutMutatingTheShip,
|
||||
AgentClaimsAndCompletesExploreTaskThroughCommands,
|
||||
|
|
@ -415,7 +416,9 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
[
|
||||
new AetheriaRuntimeBehaviorField(1, PerformanceStat(10)),
|
||||
new AetheriaRuntimeBehaviorField(2, BoolValue(false))
|
||||
]));
|
||||
]),
|
||||
new AetheriaRuntimeBehaviorPayload(3, "Wear", 0,
|
||||
[new AetheriaRuntimeBehaviorField(1, BoolValue(true))]));
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot([thermostat], [], []);
|
||||
var entity = Entity(0, 0, "player");
|
||||
entity.HeatsinksEnabled = true;
|
||||
|
|
@ -427,6 +430,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog);
|
||||
AetheriaRuntimeThermalSimulation.EnsureState(entity);
|
||||
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 0.1);
|
||||
entity.EquipmentStates.Single().Wear = 1;
|
||||
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(entity, catalog);
|
||||
var thermostatState = entity.BehaviorStates.Single(value => value.BehaviorKind == "Thermotoggle");
|
||||
RequireNear(300, thermostatState.ThermotoggleTargetTemperature, 0.000001,
|
||||
|
|
@ -444,22 +448,43 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
var before = AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 0);
|
||||
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1);
|
||||
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, 0.1);
|
||||
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(entity, catalog, 0.1);
|
||||
RequireNear(before, AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 0), 0.000001,
|
||||
"a low-pass thermotoggle above its target must stop downstream heat");
|
||||
Require(!entity.BehaviorStates.Single(value => value.BehaviorKind == "Heat").ChainReached,
|
||||
"a closed thermostat must keep downstream specialized and common behaviors unreachable");
|
||||
Require(!entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").ChainSucceeded,
|
||||
"a closed thermostat must keep the radiator Execute path from succeeding");
|
||||
RequireNear(0, entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").PumpedHeat, 0.000001,
|
||||
"a closed thermostat must keep the radiator subsystem from running");
|
||||
"a closed thermostat must not refresh Execute-owned radiator throughput");
|
||||
RequireNear(1, entity.Equipment[0].Item.Durability, 0.000001,
|
||||
"a closed thermostat must not apply trailing wear");
|
||||
|
||||
SetTarget(300, 2);
|
||||
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1);
|
||||
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, 0.1);
|
||||
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(entity, catalog, 0.1);
|
||||
Require(AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 0) > before,
|
||||
"a low-pass thermotoggle below its target must admit downstream heat");
|
||||
Require(entity.BehaviorStates.Single(value => value.BehaviorKind == "Heat").ChainReached,
|
||||
"an open thermostat must publish that the downstream behavior was reached this tick");
|
||||
RequireNear(1, entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").PumpedHeat, 0.000001,
|
||||
"an open thermostat must admit the radiator subsystem through the same chain state");
|
||||
RequireNear(0.9, entity.Equipment[0].Item.Durability, 0.000001,
|
||||
"a successful radiator must apply its authored trailing wear exactly once");
|
||||
|
||||
var radiatorPayload = thermostat.BehaviorPayloads.Single(value => value.Kind == "Radiator");
|
||||
radiatorPayload.Fields = radiatorPayload.Fields.Select(field => field.Key == 5
|
||||
? new AetheriaRuntimeBehaviorField(5, PerformanceStat(10))
|
||||
: field).ToArray();
|
||||
var durabilityBeforeEnergyRefusal = entity.Equipment[0].Item.Durability;
|
||||
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1);
|
||||
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, 0.1);
|
||||
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(entity, catalog, 0.1);
|
||||
Require(!entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").ChainSucceeded,
|
||||
"an unfunded radiator must report its fossil Execute failure");
|
||||
RequireNear(durabilityBeforeEnergyRefusal, entity.Equipment[0].Item.Durability, 0.000001,
|
||||
"an unfunded radiator must stop the chain before trailing wear");
|
||||
|
||||
void SetTarget(double target, long sequence)
|
||||
{
|
||||
|
|
@ -4732,6 +4757,45 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
"an unfunded thruster bank must not advertise a plume that did not execute");
|
||||
}
|
||||
|
||||
private static void SpecializedBehaviorSuccessGatesTrailingWear()
|
||||
{
|
||||
var ship = Entity(0, 0, "player");
|
||||
var catalog = EquipThrusterBank([ship]);
|
||||
var thrusterItem = catalog.Items.Single(value => value.ItemKey == "smoke-directional-thruster");
|
||||
thrusterItem.BehaviorPayloads =
|
||||
[
|
||||
ThrusterPayload(),
|
||||
new AetheriaRuntimeBehaviorPayload(1, "Wear", 0,
|
||||
[new AetheriaRuntimeBehaviorField(1, BoolValue(true))])
|
||||
];
|
||||
AetheriaRuntimeThermalSimulation.EnsureTopology(ship, catalog);
|
||||
AetheriaRuntimeThermalSimulation.EnsureState(ship);
|
||||
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(ship, catalog, 0.1);
|
||||
foreach (var state in ship.EquipmentStates)
|
||||
state.Wear = 1;
|
||||
|
||||
AetheriaRuntimeEnergySimulation.BeginTick(ship, catalog);
|
||||
AetheriaRuntimeBehaviorSimulation.Step(0, [ship], [], catalog, 0.1);
|
||||
AetheriaRuntimeFlightSimulation.Step([ship], [], catalog, 0.1, 1, 1, 0.001, 1);
|
||||
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(ship, catalog, 0.1);
|
||||
Require(ship.Equipment.All(slot => Math.Abs(slot.Item.Durability - 1) < 0.000001),
|
||||
"idle thrusters must return false and leave every trailing wear behavior unreachable");
|
||||
|
||||
ship.HelmForward = 1;
|
||||
AetheriaRuntimeBehaviorSimulation.Step(0, [ship], [], catalog, 0.1);
|
||||
AetheriaRuntimeFlightSimulation.Step([ship], [], catalog, 0.1, 1, 1, 0.001, 1);
|
||||
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(ship, catalog, 0.1);
|
||||
RequireEqual(2, ship.Equipment.Count(slot => slot.Item.Durability < 1),
|
||||
"only the two successful forward thrusters may resume into trailing wear");
|
||||
Require(ship.Equipment.Where(slot => slot.Item.Durability < 1)
|
||||
.All(slot => Math.Abs(slot.Item.Durability - 0.9) < 0.000001),
|
||||
"each successful specialized behavior must resume its tail exactly once");
|
||||
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(ship, catalog, 0.1);
|
||||
Require(ship.Equipment.Where(slot => slot.Item.Durability < 1)
|
||||
.All(slot => Math.Abs(slot.Item.Durability - 0.9) < 0.000001),
|
||||
"replaying deferred completion in the same tick must not apply trailing wear twice");
|
||||
}
|
||||
|
||||
private static void RareAetherDriveSpoolsOnlyOnModifiedHullFixture()
|
||||
{
|
||||
var ship = Entity(0, 0, "player");
|
||||
|
|
|
|||
|
|
@ -59,6 +59,58 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return baseline * multiplier + constant;
|
||||
}
|
||||
|
||||
public static void ReportSpecializedResult(
|
||||
AetheriaRuntimeEquippedBehavior behavior,
|
||||
bool succeeded)
|
||||
{
|
||||
if (behavior?.State == null || !behavior.State.ChainReached)
|
||||
return;
|
||||
behavior.State.ChainSucceeded = succeeded;
|
||||
}
|
||||
|
||||
public static void CompleteDeferredChains(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
double deltaSeconds)
|
||||
{
|
||||
if (entity == null || catalog == null || deltaSeconds <= 0)
|
||||
return;
|
||||
|
||||
var states = EquipmentStates(entity);
|
||||
var online = OnlineStates(entity);
|
||||
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
for (var equipmentIndex = 0; equipmentIndex < equipment.Count; equipmentIndex++)
|
||||
{
|
||||
var item = catalog.FindItem(equipment[equipmentIndex]?.Item?.ItemKey ?? "");
|
||||
var payloads = item?.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>();
|
||||
foreach (var group in Indexed(payloads).GroupBy(value => value.Payload.Group).OrderBy(value => value.Key))
|
||||
{
|
||||
var entries = group.ToArray();
|
||||
var deferredOffset = Array.FindIndex(entries, entry =>
|
||||
states.TryGetValue((equipmentIndex, entry.Index), out var state) &&
|
||||
state.ChainReached && IsDeferred(entry.Payload.Kind));
|
||||
if (deferredOffset < 0)
|
||||
continue;
|
||||
var deferredState = states[(equipmentIndex, entries[deferredOffset].Index)];
|
||||
if (!deferredState.ChainSucceeded || deferredState.ChainCompleted)
|
||||
continue;
|
||||
deferredState.ChainCompleted = true;
|
||||
|
||||
foreach (var entry in entries.Skip(deferredOffset + 1))
|
||||
{
|
||||
if (!states.TryGetValue((equipmentIndex, entry.Index), out var state))
|
||||
break;
|
||||
state.ChainReached = true;
|
||||
if (IsDeferred(entry.Payload.Kind) ||
|
||||
!ExecuteInline(entity, catalog, online, equipmentIndex, entry, state, deltaSeconds))
|
||||
break;
|
||||
}
|
||||
}
|
||||
ProjectModifierState(entity, catalog, equipmentIndex, payloads, states);
|
||||
}
|
||||
ProjectCooldownProgress(entity);
|
||||
}
|
||||
|
||||
public static int CountTargets(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
|
|
@ -109,14 +161,14 @@ namespace GameCult.Aetheria.State.Verse
|
|||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
double deltaSeconds)
|
||||
{
|
||||
var states = (entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
|
||||
.Where(value => value != null && string.Equals(value.OwnerKind,
|
||||
AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, StringComparison.Ordinal))
|
||||
.ToDictionary(value => (value.OwnerIndex, value.BehaviorIndex));
|
||||
var states = EquipmentStates(entity);
|
||||
foreach (var state in states.Values)
|
||||
{
|
||||
state.ChainReached = false;
|
||||
var online = (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.Where(value => value != null).ToDictionary(value => value.EquipmentIndex);
|
||||
state.ChainSucceeded = false;
|
||||
state.ChainCompleted = false;
|
||||
}
|
||||
var online = OnlineStates(entity);
|
||||
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
for (var equipmentIndex = 0; equipmentIndex < equipment.Count; equipmentIndex++)
|
||||
{
|
||||
|
|
@ -126,88 +178,26 @@ namespace GameCult.Aetheria.State.Verse
|
|||
(!online.TryGetValue(equipmentIndex, out var equipmentState) || equipmentState.Online);
|
||||
var payloads = item?.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>();
|
||||
|
||||
foreach (var entry in payloads.Select((payload, index) => (Payload: payload, Index: index)))
|
||||
foreach (var entry in Indexed(payloads))
|
||||
if (entry.Payload != null && string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal) &&
|
||||
states.TryGetValue((equipmentIndex, entry.Index), out var modifierState))
|
||||
modifierState.StatModifierExecuted = false;
|
||||
|
||||
if (operational)
|
||||
foreach (var group in payloads.Select((payload, index) => (Payload: payload, Index: index))
|
||||
.Where(value => value.Payload != null).GroupBy(value => value.Payload.Group).OrderBy(value => value.Key))
|
||||
foreach (var group in Indexed(payloads).GroupBy(value => value.Payload.Group).OrderBy(value => value.Key))
|
||||
foreach (var entry in group)
|
||||
{
|
||||
if (!states.TryGetValue((equipmentIndex, entry.Index), out var state))
|
||||
break;
|
||||
state.ChainReached = true;
|
||||
if (string.Equals(entry.Payload.Kind, "Switch", StringComparison.Ordinal) && !state.SwitchActivated)
|
||||
if (IsDeferred(entry.Payload.Kind) ||
|
||||
!ExecuteInline(entity, catalog, online, equipmentIndex, entry, state, deltaSeconds))
|
||||
break;
|
||||
if (string.Equals(entry.Payload.Kind, "Trigger", StringComparison.Ordinal))
|
||||
{
|
||||
if (!state.TriggerPulled)
|
||||
break;
|
||||
state.TriggerPulled = false;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Thermotoggle", StringComparison.Ordinal))
|
||||
{
|
||||
var temperature = AetheriaRuntimeThermalSimulation.EquipmentTemperature(
|
||||
entity, catalog, equipmentIndex);
|
||||
var highPass = ReadBool(entry.Payload, 2);
|
||||
if ((temperature < state.ThermotoggleTargetTemperature) ^ highPass)
|
||||
{
|
||||
// The fossil returns true for this branch.
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Cooldown", StringComparison.Ordinal))
|
||||
{
|
||||
if (state.CooldownProgress >= 0)
|
||||
break;
|
||||
state.CooldownProgress = 1;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "EnergyDraw", StringComparison.Ordinal))
|
||||
{
|
||||
var demand = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1) *
|
||||
(ReadBool(entry.Payload, 2) ? deltaSeconds : 1);
|
||||
if (!AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, demand))
|
||||
break;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Heat", StringComparison.Ordinal))
|
||||
{
|
||||
var heat = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1) *
|
||||
(ReadBool(entry.Payload, 2) ? deltaSeconds : 1);
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(entity, catalog, equipmentIndex, heat);
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "ItemUsage", StringComparison.Ordinal))
|
||||
{
|
||||
var itemKey = ReadItemKey(entry.Payload, 1);
|
||||
if (string.IsNullOrWhiteSpace(itemKey) ||
|
||||
!AetheriaRuntimeCargoTransactions.TryFind(entity, itemKey, out var cargoIndex, out var x, out var y) ||
|
||||
!AetheriaRuntimeCargoTransactions.TryRemoveQuantity(entity, cargoIndex, itemKey, x, y, 1, out _))
|
||||
break;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Wear", StringComparison.Ordinal))
|
||||
{
|
||||
var wear = online.TryGetValue(equipmentIndex, out var wearState) ? wearState.Wear : 0;
|
||||
AetheriaRuntimeThermalSimulation.ApplyWear(
|
||||
entity, equipmentIndex, wear * (ReadBool(entry.Payload, 1, true) ? deltaSeconds : 1));
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal))
|
||||
state.StatModifierExecuted = true;
|
||||
}
|
||||
|
||||
foreach (var entry in payloads.Select((payload, index) => (Payload: payload, Index: index)))
|
||||
{
|
||||
if (entry.Payload == null || !string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal) ||
|
||||
!states.TryGetValue((equipmentIndex, entry.Index), out var state))
|
||||
continue;
|
||||
state.StatModifierApplied = state.StatModifierExecuted;
|
||||
state.StatModifierTargetStatCount = CountTargets(entity, catalog, entry.Payload);
|
||||
}
|
||||
ProjectModifierState(entity, catalog, equipmentIndex, payloads, states);
|
||||
|
||||
foreach (var entry in payloads.Select((payload, index) => (Payload: payload, Index: index)))
|
||||
foreach (var entry in Indexed(payloads))
|
||||
{
|
||||
if (entry.Payload == null || !string.Equals(entry.Payload.Kind, "Cooldown", StringComparison.Ordinal) ||
|
||||
!states.TryGetValue((equipmentIndex, entry.Index), out var state))
|
||||
|
|
@ -218,6 +208,106 @@ namespace GameCult.Aetheria.State.Verse
|
|||
}
|
||||
}
|
||||
|
||||
private static bool ExecuteInline(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
IReadOnlyDictionary<int, AetheriaRuntimeEquipmentStateCommit> online,
|
||||
int equipmentIndex,
|
||||
(AetheriaRuntimeBehaviorPayload Payload, int Index) entry,
|
||||
AetheriaRuntimeBehaviorStateCommit state,
|
||||
double deltaSeconds)
|
||||
{
|
||||
if (string.Equals(entry.Payload.Kind, "Switch", StringComparison.Ordinal) && !state.SwitchActivated)
|
||||
return false;
|
||||
if (string.Equals(entry.Payload.Kind, "Trigger", StringComparison.Ordinal))
|
||||
{
|
||||
if (!state.TriggerPulled)
|
||||
return false;
|
||||
state.TriggerPulled = false;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Thermotoggle", StringComparison.Ordinal))
|
||||
{
|
||||
var temperature = AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, equipmentIndex);
|
||||
if (!((temperature < state.ThermotoggleTargetTemperature) ^ ReadBool(entry.Payload, 2)))
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Cooldown", StringComparison.Ordinal))
|
||||
{
|
||||
if (state.CooldownProgress >= 0)
|
||||
return false;
|
||||
state.CooldownProgress = 1;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "EnergyDraw", StringComparison.Ordinal))
|
||||
{
|
||||
var demand = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1) *
|
||||
(ReadBool(entry.Payload, 2) ? deltaSeconds : 1);
|
||||
if (!AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, demand))
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Heat", StringComparison.Ordinal))
|
||||
{
|
||||
var heat = Evaluate(entity, catalog, equipmentIndex, entry.Index, entry.Payload, state, 1) *
|
||||
(ReadBool(entry.Payload, 2) ? deltaSeconds : 1);
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(entity, catalog, equipmentIndex, heat);
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "ItemUsage", StringComparison.Ordinal))
|
||||
{
|
||||
var itemKey = ReadItemKey(entry.Payload, 1);
|
||||
if (string.IsNullOrWhiteSpace(itemKey) ||
|
||||
!AetheriaRuntimeCargoTransactions.TryFind(entity, itemKey, out var cargoIndex, out var x, out var y) ||
|
||||
!AetheriaRuntimeCargoTransactions.TryRemoveQuantity(entity, cargoIndex, itemKey, x, y, 1, out _))
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "Wear", StringComparison.Ordinal))
|
||||
{
|
||||
var wear = online.TryGetValue(equipmentIndex, out var wearState) ? wearState.Wear : 0;
|
||||
AetheriaRuntimeThermalSimulation.ApplyWear(
|
||||
entity, equipmentIndex, wear * (ReadBool(entry.Payload, 1, true) ? deltaSeconds : 1));
|
||||
}
|
||||
if (string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal))
|
||||
state.StatModifierExecuted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsDeferred(string? kind) =>
|
||||
string.Equals(kind, AetheriaRuntimeBehaviorKinds.Thruster, StringComparison.Ordinal) ||
|
||||
string.Equals(kind, AetheriaRuntimeBehaviorKinds.AetherDrive, StringComparison.Ordinal) ||
|
||||
string.Equals(kind, "Radiator", StringComparison.Ordinal);
|
||||
|
||||
private static IEnumerable<(AetheriaRuntimeBehaviorPayload Payload, int Index)> Indexed(
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorPayload> payloads) =>
|
||||
payloads.Select((payload, index) => (Payload: payload, Index: index))
|
||||
.Where(value => value.Payload != null);
|
||||
|
||||
private static Dictionary<(int OwnerIndex, int BehaviorIndex), AetheriaRuntimeBehaviorStateCommit>
|
||||
EquipmentStates(AetheriaRuntimeEntitySnapshotCommit entity) =>
|
||||
(entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
|
||||
.Where(value => value != null && string.Equals(value.OwnerKind,
|
||||
AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, StringComparison.Ordinal))
|
||||
.ToDictionary(value => (value.OwnerIndex, value.BehaviorIndex));
|
||||
|
||||
private static Dictionary<int, AetheriaRuntimeEquipmentStateCommit> OnlineStates(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity) =>
|
||||
(entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.Where(value => value != null).ToDictionary(value => value.EquipmentIndex);
|
||||
|
||||
private static void ProjectModifierState(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
int equipmentIndex,
|
||||
IReadOnlyList<AetheriaRuntimeBehaviorPayload> payloads,
|
||||
IReadOnlyDictionary<(int OwnerIndex, int BehaviorIndex), AetheriaRuntimeBehaviorStateCommit> states)
|
||||
{
|
||||
foreach (var entry in Indexed(payloads))
|
||||
{
|
||||
if (!string.Equals(entry.Payload.Kind, "StatModifier", StringComparison.Ordinal) ||
|
||||
!states.TryGetValue((equipmentIndex, entry.Index), out var state))
|
||||
continue;
|
||||
state.StatModifierApplied = state.StatModifierExecuted;
|
||||
state.StatModifierTargetStatCount = CountTargets(entity, catalog, entry.Payload);
|
||||
}
|
||||
}
|
||||
|
||||
private static double Evaluate(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
|
|
|
|||
|
|
@ -638,6 +638,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
foreach (var entity in entities)
|
||||
{
|
||||
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, deltaSeconds);
|
||||
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(entity, catalog, deltaSeconds);
|
||||
AetheriaRuntimeEnergySimulation.SettleReactors(entity, catalog, deltaSeconds);
|
||||
AetheriaRuntimeThermalSimulation.Step(entity, deltaSeconds, catalog);
|
||||
var medical = AetheriaRuntimeThermalMedicalSimulation.Step(
|
||||
|
|
|
|||
|
|
@ -62,31 +62,37 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public static void StepRadiators(AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog, double deltaSeconds)
|
||||
{
|
||||
if (entity == null || catalog == null || deltaSeconds <= 0 || !entity.HeatsinksEnabled) return;
|
||||
foreach (var radiator in AetheriaRuntimeEquippedBehaviorQueries.FindExecuting(entity, catalog, "Radiator"))
|
||||
if (entity == null || catalog == null || deltaSeconds <= 0) return;
|
||||
foreach (var radiator in AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, "Radiator"))
|
||||
{
|
||||
var itemTemperature = AetheriaRuntimeThermalSimulation.EquipmentTemperature(
|
||||
entity, catalog, radiator.EquipmentIndex);
|
||||
if (radiator.State.RadiatorTemperature <= 0)
|
||||
radiator.State.RadiatorTemperature = itemTemperature;
|
||||
radiator.State.PumpedHeat = Math.Max(0, radiator.EvaluateStat(2));
|
||||
radiator.State.WasteHeat = Math.Max(0, radiator.EvaluateStat(4));
|
||||
radiator.State.EnergyUsage = Math.Max(0, radiator.EvaluateStat(5));
|
||||
var tempRatio = Math.Max(radiator.State.RadiatorTemperature / Math.Max(0.000001, itemTemperature), 1);
|
||||
if (radiator.State.WasteHeat > 0 &&
|
||||
tempRatio > radiator.State.PumpedHeat / radiator.State.WasteHeat)
|
||||
continue;
|
||||
if (!TryConsume(entity, catalog, radiator.State.EnergyUsage * tempRatio * deltaSeconds))
|
||||
continue;
|
||||
var temperatureFloor = ReadNumber(radiator.Payload, 3);
|
||||
var pumped = radiator.State.PumpedHeat * Math.Max(itemTemperature - temperatureFloor, 0);
|
||||
if (pumped >= 0.01)
|
||||
if (entity.HeatsinksEnabled && radiator.State.ChainReached)
|
||||
{
|
||||
var waste = radiator.State.WasteHeat * tempRatio;
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(
|
||||
entity, catalog, radiator.EquipmentIndex, (waste - pumped) * deltaSeconds);
|
||||
var thermalMass = Math.Max(0.000001, radiator.EvaluateStat(6));
|
||||
radiator.State.RadiatorTemperature += pumped / thermalMass * deltaSeconds;
|
||||
radiator.State.PumpedHeat = Math.Max(0, radiator.EvaluateStat(2));
|
||||
radiator.State.WasteHeat = Math.Max(0, radiator.EvaluateStat(4));
|
||||
radiator.State.EnergyUsage = Math.Max(0, radiator.EvaluateStat(5));
|
||||
var tempRatio = Math.Max(radiator.State.RadiatorTemperature / Math.Max(0.000001, itemTemperature), 1);
|
||||
var overwhelmed = radiator.State.WasteHeat > 0 &&
|
||||
tempRatio > radiator.State.PumpedHeat / radiator.State.WasteHeat;
|
||||
var succeeded = overwhelmed ||
|
||||
TryConsume(entity, catalog, radiator.State.EnergyUsage * tempRatio * deltaSeconds);
|
||||
AetheriaRuntimeBehaviorSimulation.ReportSpecializedResult(radiator, succeeded);
|
||||
if (succeeded && !overwhelmed)
|
||||
{
|
||||
var temperatureFloor = ReadNumber(radiator.Payload, 3);
|
||||
var pumped = radiator.State.PumpedHeat * Math.Max(itemTemperature - temperatureFloor, 0);
|
||||
if (pumped >= 0.01)
|
||||
{
|
||||
var waste = radiator.State.WasteHeat * tempRatio;
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(
|
||||
entity, catalog, radiator.EquipmentIndex, (waste - pumped) * deltaSeconds);
|
||||
var thermalMass = Math.Max(0.000001, radiator.EvaluateStat(6));
|
||||
radiator.State.RadiatorTemperature += pumped / thermalMass * deltaSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
radiator.State.Emissivity = Math.Max(0, radiator.EvaluateStat(1));
|
||||
var radiation = Math.Pow(Math.Max(0, radiator.State.RadiatorTemperature),
|
||||
|
|
|
|||
|
|
@ -42,17 +42,23 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
var mass = ResolveMass(entity, byIndex, catalog, new HashSet<int>());
|
||||
var deltaRotation = ResolveTurnAxis(entity, deltaSeconds);
|
||||
var thrusters = Online(entity, catalog, AetheriaRuntimeBehaviorKinds.Thruster);
|
||||
var thrusters = Executing(entity, catalog, AetheriaRuntimeBehaviorKinds.Thruster);
|
||||
ConfigureThrusterAxes(entity, thrusters, catalog, deltaRotation, torqueFloor);
|
||||
var activeThrusters = new List<AetheriaRuntimeEquippedBehavior>();
|
||||
foreach (var thruster in thrusters)
|
||||
if (StepThruster(entity, thruster, mass, deltaSeconds, catalog, torqueMultiplier))
|
||||
{
|
||||
var succeeded = StepThruster(entity, thruster, mass, deltaSeconds, catalog, torqueMultiplier);
|
||||
AetheriaRuntimeBehaviorSimulation.ReportSpecializedResult(thruster, succeeded);
|
||||
if (succeeded)
|
||||
activeThrusters.Add(thruster);
|
||||
}
|
||||
ApplyThrusterVisibility(entity, activeThrusters);
|
||||
SetDriveAxes(entity, catalog, deltaRotation);
|
||||
foreach (var drive in Online(entity, catalog, AetheriaRuntimeBehaviorKinds.AetherDrive))
|
||||
StepAetherDrive(entity, drive, mass, deltaSeconds, catalog,
|
||||
aetherTorqueMultiplier, aetherHeatMultiplier);
|
||||
foreach (var drive in Executing(entity, catalog, AetheriaRuntimeBehaviorKinds.AetherDrive))
|
||||
AetheriaRuntimeBehaviorSimulation.ReportSpecializedResult(
|
||||
drive,
|
||||
StepAetherDrive(entity, drive, mass, deltaSeconds, catalog,
|
||||
aetherTorqueMultiplier, aetherHeatMultiplier));
|
||||
ApplyHullDrag(entity, catalog, deltaSeconds);
|
||||
ApplyVelocityBehaviors(entity, catalog, deltaSeconds);
|
||||
}
|
||||
|
|
@ -326,7 +332,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return AetheriaRuntimeEquipmentRotation.QuarterTurns(behavior.Slot?.Rotation);
|
||||
}
|
||||
|
||||
private static void StepAetherDrive(
|
||||
private static bool StepAetherDrive(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeEquippedBehavior drive,
|
||||
double entityMass,
|
||||
|
|
@ -396,7 +402,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var torqueRatio = DivideOrZero(actualRpmDelta, potentialRpmDelta);
|
||||
var energyDraw = Math.Max(0, drive.EvaluateStat(9, ThermalPerformance(entity, drive.EquipmentIndex)));
|
||||
var draw = (torqueRatio.X + torqueRatio.Y + torqueRatio.Z) * energyDraw / 3 * deltaSeconds;
|
||||
if (AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, draw))
|
||||
var succeeded = AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, draw);
|
||||
if (succeeded)
|
||||
rpm += actualRpmDelta;
|
||||
|
||||
drive.State.AetherDriveThrustX = thrust.X;
|
||||
|
|
@ -408,6 +415,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
drive.State.AetherDriveMaximumRpm = maximumRpm;
|
||||
drive.State.AetherDriveThrustDirectionX = thrustDirection.X;
|
||||
drive.State.AetherDriveThrustDirectionY = thrustDirection.Y;
|
||||
return succeeded;
|
||||
}
|
||||
|
||||
private static void ApplyHullDrag(
|
||||
|
|
@ -457,6 +465,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
string kind) =>
|
||||
AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, kind);
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeEquippedBehavior> Executing(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
string kind) =>
|
||||
AetheriaRuntimeEquippedBehaviorQueries.FindExecuting(entity, catalog, kind);
|
||||
|
||||
private static double ThermalPerformance(AetheriaRuntimeEntitySnapshotCommit entity, int equipmentIndex) =>
|
||||
(entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.FirstOrDefault(state => state != null && state.EquipmentIndex == equipmentIndex)?
|
||||
|
|
|
|||
|
|
@ -1521,6 +1521,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
[Key(59)]
|
||||
public bool ChainReached { get; set; }
|
||||
|
||||
[Key(60)]
|
||||
public bool ChainSucceeded { get; set; }
|
||||
|
||||
[Key(61)]
|
||||
public bool ChainCompleted { get; set; }
|
||||
|
||||
[Key(55)]
|
||||
public string ResourceScannerTargetBodyKey { get; set; } = "";
|
||||
|
||||
|
|
|
|||
|
|
@ -326,9 +326,13 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
|
|||
Each behavior row also records whether ordered execution reached it during
|
||||
the current fixed step. Specialized subsystem handlers consume that daemon
|
||||
fact instead of equating "installed and online" with "admitted by the
|
||||
chain". Radiators are the first migrated consumer: a closed thermostat makes
|
||||
its downstream radiator unreachable, so the energy/thermal handler cannot
|
||||
pump heat behind the gate.
|
||||
chain". `Thruster`, `AetherDrive`, and `Radiator` pause their authored group,
|
||||
report the fossil `Execute` return value from their owning simulation organ,
|
||||
and let the behavior owner resume the common tail at most once. A failed
|
||||
energy draw or idle thruster therefore cannot consume trailing wear. A closed
|
||||
thermostat makes its radiator unreachable; an admitted but unfunded radiator
|
||||
reports failure and also stops the tail. Radiator emissive cooling remains an
|
||||
always-update effect, matching the fossil independently of chain admission.
|
||||
- Sensor contact information is daemon-owned. Each operational installed
|
||||
`Sensor` behavior executes the fossil information equation against target
|
||||
visibility, distance, observer-relative angle, its authored sensitivity
|
||||
|
|
@ -2678,7 +2682,9 @@ First Aetheria surfaces to publish:
|
|||
equipment-local temperature/performance, shutdown, wear, conduction, and
|
||||
radiation. The daemon orders radiator pumping before reactor settlement and
|
||||
thermal conduction; client operations have no direct energy, heat, wear, or
|
||||
equipment-online writer. `AetheriaRuntimeThermalMedicalSimulation` derives
|
||||
equipment-online writer. Radiator pumping reports its exact chain result,
|
||||
while emissive cooling remains the behavior's independent always-update path.
|
||||
`AetheriaRuntimeThermalMedicalSimulation` derives
|
||||
cockpit exposure, recovery, severe-risk crossings, and thermal death from the
|
||||
cockpit's canonical cells; all death causes use the same destruction commit.
|
||||
The Eve game surface publishes the resulting power, radiator, temperature,
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Save/resume | original persistence paths | daemon/CultCache | published current state | clients rehydrate | owner-exists; not parity-proven | mid-loop restart |
|
||||
| Terminus loop | original game composition | daemon/Ymir/Eve | pilot surface | Aetheria.Unity | live minimum-loop proof: generic Unity discovers the provider, lowers the pilot world/assets, single-steps dock and undock while paused, buys daemon-priced station stock, single-steps Ymir salvage collection, resumes, moves, targets, fires, and presents authoritative receipts/shot outcome; the released full-session profile starts with no pickup, destroys a deterministic target, observes its canonical cargo drop, tractors it into a Ymir Begin contact, and proves exactly one `ymir-fact:` collection receipt plus cargo `0` to `1` | richer trade content, docking transitions, and sustained combat encounters |
|
||||
| Starbridge | original/shared architecture intent | one daemon session | pilot + commander surfaces | Unity + Electron | commander task board and autonomous explore execution wired; Electron Eve lowering and split-target proof missing | commander orders plus four pilots and autonomous workers |
|
||||
| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; `Switch`, one-shot `Trigger`, normalized `Cooldown`, adjustable high/low-pass `Thermotoggle`, `EnergyDraw`, equipment `Heat`, atomic `ItemUsage`, and thermal `Wear` execute in authored group order and stop downstream effects on refusal; each row records whether the chain reached it, and the radiator subsystem now requires that fact rather than mere installation. Cooldown projects generic progress and thermostat target is daemon-commanded state seeded from authored data. Equipped and timed-consumable `StatModifier` sources persist daemon-owned lifecycle state and every runtime stat reader applies the fossil `base × multiplier product + constant sum` rule through one query; legacy `Data` kind references and required-behavior filters are honored | thruster/AetherDrive/radiator success gating their trailing wear, remaining specialized return-value composition, and canonical consumable-content proof |
|
||||
| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; `Switch`, one-shot `Trigger`, normalized `Cooldown`, adjustable high/low-pass `Thermotoggle`, `EnergyDraw`, equipment `Heat`, atomic `ItemUsage`, and thermal `Wear` execute in authored group order and stop downstream effects on refusal; each row records whether the chain reached it. `Thruster`, rare `AetherDrive`, and `Radiator` now pause their groups at the specialized owner, report the exact fossil success result, and resume common tails at most once, so idle or unfunded actuators cannot consume wear; radiator emission remains its independent always-update path. Cooldown projects generic progress and thermostat target is daemon-commanded state seeded from authored data. Equipped and timed-consumable `StatModifier` sources persist daemon-owned lifecycle state and every runtime stat reader applies the fossil `base × multiplier product + constant sum` rule through one query; legacy `Data` kind references and required-behavior filters are honored | remaining specialized return-value composition and canonical consumable-content proof |
|
||||
| Sensors/visibility | installed sensor sensitivity and angular curve, `Visibility`, `Reflector`, decaying signature sources, stellar light, ping state, and observer information thresholds | daemon | accumulated contacts, named visibility sources, and ping events | indicators/maps and provider environment lowering | every operational installed Sensor executes the fossil visibility/sensitivity/angle/distance information equation and decay; visibility is thresholded by the provider setting. Actor-scoped ping owns energy, cooldown, expansion radius, reconnect-stable crossed-target state, one-time information boost, and transient signature. Operational `Visibility` equipment publishes evaluated constant signature; committed weapons publish authored signature including charged-shot multiplication; transient weapon and ping sources decay by the fossil exponential rule. Reflectors multiply evaluated cross-section by the exponent-8 stellar `PowerPulse` after thermal settlement. Gameplay light and environment projection share resolved authored orbit/body centers and provider-owned sun radius inputs. Contact refresh no longer invents perfect information or overwrites emitted visibility; docked craft consume one daemon-derived union of craft and dock-parent contacts through both Eve rows and SoA bodies, with sensor-source provenance | active consumable visibility/reflector effects, complete source cardinality, classification timelines, and canonical catalog station-array data |
|
||||
| AI/tasks | corporation task scheduler plus agent state machines and installed orbital `TurretController` | daemon | commander task board, corporation survey ledger, controller aim/state, and shared semantic controls | Eve commander/pilot clients | point exploration, loadout-driven attack, haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home are wired through shared commands. Ship combat samples the fossil DPS/range policy, selects live weapon groups, leads projectiles, strafes relative to target attitude, and gates fire on catalog placement rotation. Operational orbital controllers independently acquire the first visible hostile ship, publish predictive look direction, and trigger only exact aligned in-range weapon behaviors through the shared combat lifecycle; non-player identity no longer grants implicit group-zero fire. Cross-zone task and home routes survive the full daemon-owned wormhole timeline; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing | cancellation, reconnect, multi-client command transfer, and model-local barrel-direction metadata |
|
||||
| Consumables | cargo-backed timed behavior containers | daemon | dynamic actions, active effects, and lifecycle facts | action bar/schematic | daemon atomically resolves typed metadata and concrete cargo, preserves consumed-instance quality, enforces authored stacking, evaluates effectiveness/quality, and executes ordered `EnergyDraw`, fossil-compatible no-op `Heat`, `ItemUsage`, and stateful `Cooldown` payloads after energy tick initialization; every effect and payload state has stable authored identity; cooldowns update before the ordered chain, require state strictly below zero, reset to one on success, and continue aging behind earlier failures; false and unsupported payloads stop later execution with a typed fact; timers retain the fossil's exact-zero final tick; Unity reconciles presentation by effect identity and cannot restore active behavior state by list index | catalog revision pinning, stat modifiers, remaining behavior families, and reconnect presentation proof |
|
||||
|
|
|
|||
Loading…
Reference in a new issue