Project behavior state from daemon catalog payloads
This commit is contained in:
parent
911bb72f69
commit
62f9bccfc1
5 changed files with 277 additions and 89 deletions
|
|
@ -1952,6 +1952,7 @@ static async Task<AetheriaRuntimeRunCheckpointCommit?> ReadRuntimeRunCheckpointA
|
|||
if (run == null)
|
||||
return null;
|
||||
|
||||
var catalog = node.RuntimeCatalog().Latest() ?? new AetheriaRuntimeCatalogSnapshot();
|
||||
var zones = new List<AetheriaRuntimeZoneSnapshotCommit>();
|
||||
var zoneKeys = run.ZoneKeys ?? Array.Empty<string>();
|
||||
for (var zoneIndex = 0; zoneIndex < zoneKeys.Length; zoneIndex++)
|
||||
|
|
@ -1960,7 +1961,7 @@ static async Task<AetheriaRuntimeRunCheckpointCommit?> ReadRuntimeRunCheckpointA
|
|||
if (zone == null)
|
||||
continue;
|
||||
|
||||
zones.Add(await ToRuntimeZoneAsync(node, zone, zoneIndex, renderSettings).ConfigureAwait(false));
|
||||
zones.Add(await ToRuntimeZoneAsync(node, zone, zoneIndex, renderSettings, catalog).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeRunCheckpointCommit
|
||||
|
|
@ -1990,7 +1991,8 @@ static async Task<AetheriaRuntimeZoneSnapshotCommit> ToRuntimeZoneAsync(
|
|||
AetheriaStateNode node,
|
||||
AetheriaZoneState zone,
|
||||
int zoneIndex,
|
||||
AetheriaRuntimeDaemonRenderSettings renderSettings)
|
||||
AetheriaRuntimeDaemonRenderSettings renderSettings,
|
||||
AetheriaRuntimeCatalogSnapshot catalog)
|
||||
{
|
||||
var entityKeys = zone.EntityKeys ?? Array.Empty<string>();
|
||||
var entityIndices = entityKeys
|
||||
|
|
@ -2002,7 +2004,7 @@ static async Task<AetheriaRuntimeZoneSnapshotCommit> ToRuntimeZoneAsync(
|
|||
{
|
||||
var entity = await node.MutableDocument<AetheriaEntitySnapshot>(new CultRecordKey(entityKeys[entityIndex])).ReadAsync().ConfigureAwait(false);
|
||||
if (entity != null)
|
||||
entities.Add(ToRuntimeEntity(entity, entityIndex, entityIndices));
|
||||
entities.Add(ToRuntimeEntity(entity, entityIndex, entityIndices, catalog));
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeZoneSnapshotCommit
|
||||
|
|
@ -2030,9 +2032,11 @@ static async Task<AetheriaRuntimeZoneSnapshotCommit> ToRuntimeZoneAsync(
|
|||
static AetheriaRuntimeEntitySnapshotCommit ToRuntimeEntity(
|
||||
AetheriaEntitySnapshot entity,
|
||||
int entityIndex,
|
||||
IReadOnlyDictionary<string, int> entityIndices)
|
||||
IReadOnlyDictionary<string, int> entityIndices,
|
||||
AetheriaRuntimeCatalogSnapshot catalog)
|
||||
{
|
||||
return new AetheriaRuntimeEntitySnapshotCommit
|
||||
var equipment = ToEntitySlotCommits(entity.Equipment);
|
||||
var runtimeEntity = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = entityIndex,
|
||||
Name = entity.Name ?? "",
|
||||
|
|
@ -2044,7 +2048,7 @@ static AetheriaRuntimeEntitySnapshotCommit ToRuntimeEntity(
|
|||
DirectionY = entity.Direction?.Y ?? 1,
|
||||
FactionKey = entity.FactionKey ?? "",
|
||||
HullItemKey = entity.HullItemKey ?? "",
|
||||
Equipment = ToEntitySlotCommits(entity.Equipment),
|
||||
Equipment = equipment,
|
||||
CargoBays = ToEntitySlotCommits(entity.CargoBays),
|
||||
DockingBays = ToEntitySlotCommits(entity.DockingBays),
|
||||
ChildEntityIndices = (entity.ChildEntityKeys ?? Array.Empty<string>())
|
||||
|
|
@ -2092,7 +2096,7 @@ static AetheriaRuntimeEntitySnapshotCommit ToRuntimeEntity(
|
|||
})
|
||||
.ToArray(),
|
||||
WeaponStates = Array.Empty<AetheriaRuntimeWeaponStateCommit>(),
|
||||
BehaviorStates = Array.Empty<AetheriaRuntimeBehaviorStateCommit>(),
|
||||
BehaviorStates = AetheriaRuntimeBehaviorStateProjector.CreateEquipmentBehaviorStates(equipment, catalog),
|
||||
CargoContents = ToCargoBayCommits(entity.CargoContents),
|
||||
DockingBayContents = ToCargoBayCommits(entity.DockingBayContents),
|
||||
DockingBayAssignments = (entity.DockingBayAssignments ?? Array.Empty<int>()).ToArray(),
|
||||
|
|
@ -2103,6 +2107,8 @@ static AetheriaRuntimeEntitySnapshotCommit ToRuntimeEntity(
|
|||
.Where(contact => contact.TargetEntityIndex >= 0)
|
||||
.ToArray()
|
||||
};
|
||||
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(runtimeEntity, catalog);
|
||||
return runtimeEntity;
|
||||
}
|
||||
|
||||
static AetheriaRuntimeEntityContactCommit ToRuntimeContact(
|
||||
|
|
|
|||
|
|
@ -576,7 +576,7 @@ Console.WriteLine($"Name files: {nameFiles.Length}");
|
|||
Console.WriteLine("Live gameplay source purity: no serializer or legacy database symbols in Assets/Scripts");
|
||||
Console.WriteLine("Package serializer boundary: MessagePack execution remains in named CultCache transport files; document DTOs may carry schema annotations");
|
||||
Console.WriteLine("Shared Eve package ownership: generic Unity Eve packages import from the neighboring Eve repo instead of local staged copies");
|
||||
Console.WriteLine("Typed runtime behavior coverage: live behavior kinds have typed factory plus progress/state restore coverage");
|
||||
Console.WriteLine("Typed runtime behavior coverage: daemon behavior-state rows derive from catalog payloads; Unity behavior objects are presentation restore only");
|
||||
Console.WriteLine("Zone construction/runtime key authority: body/orbit wrappers and task shells retain typed keys instead of GUID sidecars");
|
||||
Console.WriteLine("Eve runtime bootstrap: daemon-published game surface mounts through UI Toolkit presenter");
|
||||
Console.WriteLine("Renderer-local console authority: deleted; UI commands flow through Eve command documents");
|
||||
|
|
@ -3151,98 +3151,69 @@ static void RequireFactionKeyIdentity(string root)
|
|||
|
||||
static void RequireTypedRuntimeBehaviorCoverage(string root)
|
||||
{
|
||||
var itemManagerPath = Path.Combine(root, "Assets", "Scripts", "ServerShared", "ItemManager.cs");
|
||||
var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs");
|
||||
var observedEntityRestorerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "AetheriaUnityObservedEntityRestorer.cs");
|
||||
var behaviorStateProjectorPath = Path.Combine(
|
||||
root,
|
||||
"Packages",
|
||||
"org.gamecult.aetheria.state",
|
||||
"Runtime",
|
||||
"AetheriaRuntimeBehaviorStateProjector.cs");
|
||||
var daemonProgramPath = Path.Combine(root, "Aetheria.State.Daemon", "Program.cs");
|
||||
var unityProjectPath = Path.Combine(root, "GameCult.Aetheria.State.Unity.csproj");
|
||||
|
||||
var itemManager = File.Exists(itemManagerPath)
|
||||
? File.ReadAllText(itemManagerPath)
|
||||
: throw new InvalidOperationException("Cannot verify typed runtime behavior coverage; ItemManager.cs is missing.");
|
||||
var actionGameManager = File.Exists(actionGameManagerPath)
|
||||
? File.ReadAllText(actionGameManagerPath)
|
||||
: throw new InvalidOperationException("Cannot verify typed runtime behavior coverage; ActionGameManager.cs is missing.");
|
||||
var observedEntityRestorer = File.Exists(observedEntityRestorerPath)
|
||||
? File.ReadAllText(observedEntityRestorerPath)
|
||||
: throw new InvalidOperationException("Cannot verify typed runtime behavior coverage; AetheriaUnityObservedEntityRestorer.cs is missing.");
|
||||
var behaviorStateProjector = File.Exists(behaviorStateProjectorPath)
|
||||
? File.ReadAllText(behaviorStateProjectorPath)
|
||||
: throw new InvalidOperationException("Cannot verify typed runtime behavior coverage; behavior-state projector is missing.");
|
||||
var daemonProgram = File.Exists(daemonProgramPath)
|
||||
? File.ReadAllText(daemonProgramPath)
|
||||
: throw new InvalidOperationException("Cannot verify typed runtime behavior coverage; daemon program is missing.");
|
||||
var unityProject = File.Exists(unityProjectPath)
|
||||
? File.ReadAllText(unityProjectPath)
|
||||
: throw new InvalidOperationException("Cannot verify typed runtime behavior coverage; Unity project is missing.");
|
||||
var runtimeRestoreSource = actionGameManager + "\n" + observedEntityRestorer;
|
||||
|
||||
var requiredFactoryMappings = new[]
|
||||
var requiredBehaviorStateProjection = new[]
|
||||
{
|
||||
"case \"AetherDrive\": return new AetherDrive(definition, item);",
|
||||
"case \"AutoWeapon\": return new AutoWeapon(definition, item);",
|
||||
"case \"Capacitor\": return new Capacitor(definition, item);",
|
||||
"case \"ChargedWeapon\": return new ChargedWeapon(definition, item);",
|
||||
"case \"Cockpit\": return new Cockpit(definition, item);",
|
||||
"case \"Cooldown\": return new Cooldown(definition, item);",
|
||||
"case \"ConstantWeapon\": return new ConstantWeapon(definition, item);",
|
||||
"case \"EnergyDraw\": return new EnergyDraw(definition, item);",
|
||||
"case \"GuidedWeapon\": return new InstantWeapon(definition, item);",
|
||||
"case \"Heat\": return new Heat(definition, item);",
|
||||
"case \"HeatStorage\": return new HeatStorage(definition, item);",
|
||||
"case \"InstantWeapon\": return new InstantWeapon(definition, item);",
|
||||
"case \"ItemUsage\": return new ItemUsage(definition, item);",
|
||||
"case \"Launcher\": return new LockWeapon(definition, item);",
|
||||
"case \"LockWeapon\": return new LockWeapon(definition, item);",
|
||||
"case \"MiningTool\": return new MiningTool(definition, item);",
|
||||
"case \"Radiator\": return new Radiator(definition, item);",
|
||||
"case \"Reactor\": return new Reactor(definition, item);",
|
||||
"case \"Reflector\": return new Reflector(definition, item);",
|
||||
"case \"ResourceScanner\": return new ResourceScanner(definition, item);",
|
||||
"case \"Sensor\": return new Sensor(definition, item);",
|
||||
"case \"Shield\": return new Shield(definition, item);",
|
||||
"case \"StatModifier\": return new StatModifier(definition, item);",
|
||||
"case \"Switch\": return new Switch(definition, item);",
|
||||
"case \"Thermotoggle\": return new Thermotoggle(definition, item);",
|
||||
"case \"Thruster\": return new Thruster(definition, item);",
|
||||
"case \"Trigger\": return new Trigger(definition, item);",
|
||||
"case \"TurretController\": return new TurretController(definition, item);",
|
||||
"case \"VelocityConversion\": return new VelocityConversion(definition, item);",
|
||||
"case \"VelocityLimit\": return new VelocityLimit(definition, item);",
|
||||
"case \"Visibility\": return new Visibility(definition, item);",
|
||||
"case \"Wear\": return new Wear(definition, item);",
|
||||
"case \"AetherDrive\": return new AetherDrive(definition, effect);",
|
||||
"case \"AutoWeapon\": return new InstantWeapon(definition, effect);",
|
||||
"case \"Capacitor\": return new Capacitor(definition, effect);",
|
||||
"case \"ChargedWeapon\": return new ChargedWeapon(definition, effect);",
|
||||
"case \"Cockpit\": return new Cockpit(definition, effect);",
|
||||
"case \"Cooldown\": return new Cooldown(definition, effect);",
|
||||
"case \"ConstantWeapon\": return new ConstantWeapon(definition, effect);",
|
||||
"case \"EnergyDraw\": return new EnergyDraw(definition, effect);",
|
||||
"case \"GuidedWeapon\": return new InstantWeapon(definition, effect);",
|
||||
"case \"Heat\": return new Heat(definition, effect);",
|
||||
"case \"HeatStorage\": return new HeatStorage(definition, effect);",
|
||||
"case \"InstantWeapon\": return new InstantWeapon(definition, effect);",
|
||||
"case \"ItemUsage\": return new ItemUsage(definition, effect);",
|
||||
"case \"Launcher\": return new LockWeapon(definition, effect);",
|
||||
"case \"LockWeapon\": return new LockWeapon(definition, effect);",
|
||||
"case \"MiningTool\": return new MiningTool(definition, effect);",
|
||||
"case \"Radiator\": return new Radiator(definition, effect);",
|
||||
"case \"Reactor\": return new Reactor(definition, effect);",
|
||||
"case \"Reflector\": return new Reflector(definition, effect);",
|
||||
"case \"ResourceScanner\": return new ResourceScanner(definition, effect);",
|
||||
"case \"Sensor\": return new Sensor(definition, effect);",
|
||||
"case \"Shield\": return new Shield(definition, effect);",
|
||||
"case \"StatModifier\": return new StatModifier(definition, effect);",
|
||||
"case \"Switch\": return new Switch(definition, effect);",
|
||||
"case \"Thermotoggle\": return new Thermotoggle(definition, effect);",
|
||||
"case \"Thruster\": return new Thruster(definition, effect);",
|
||||
"case \"Trigger\": return new Trigger(definition, effect);",
|
||||
"case \"TurretController\": return new TurretController(definition, effect);",
|
||||
"case \"VelocityConversion\": return new VelocityConversion(definition, effect);",
|
||||
"case \"VelocityLimit\": return new VelocityLimit(definition, effect);",
|
||||
"case \"Visibility\": return new Visibility(definition, effect);",
|
||||
"case \"Wear\": return new Wear(definition, effect);"
|
||||
"public static class AetheriaRuntimeBehaviorStateProjector",
|
||||
"public static void EnsureEquipmentBehaviorStates(",
|
||||
"public static AetheriaRuntimeBehaviorStateCommit[] CreateEquipmentBehaviorStates(",
|
||||
"AetheriaRuntimeCatalogSnapshot? catalog",
|
||||
"catalog.FindItem(item.ItemKey ?? \"\")",
|
||||
"typedItem?.BehaviorPayloads",
|
||||
"OwnerKind = EquipmentOwnerKind",
|
||||
"BehaviorKind = payload.Kind"
|
||||
};
|
||||
|
||||
var missingFactoryMappings = requiredFactoryMappings
|
||||
.Where(symbol => !itemManager.Contains(symbol, StringComparison.Ordinal))
|
||||
var missingBehaviorStateProjection = requiredBehaviorStateProjection
|
||||
.Where(symbol => !behaviorStateProjector.Contains(symbol, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
|
||||
if (missingFactoryMappings.Length > 0)
|
||||
if (missingBehaviorStateProjection.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Typed runtime behavior factory no longer covers the live behavior payload kinds: " +
|
||||
string.Join(", ", missingFactoryMappings));
|
||||
"Typed runtime behavior authority must derive daemon behavior-state rows from catalog payloads instead of Unity behavior objects: " +
|
||||
string.Join(", ", missingBehaviorStateProjection));
|
||||
}
|
||||
|
||||
if (!daemonProgram.Contains("node.RuntimeCatalog().Latest() ?? new AetheriaRuntimeCatalogSnapshot()", StringComparison.Ordinal) ||
|
||||
!daemonProgram.Contains("AetheriaRuntimeBehaviorStateProjector.CreateEquipmentBehaviorStates(equipment, catalog)", StringComparison.Ordinal) ||
|
||||
!daemonProgram.Contains("AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(runtimeEntity, catalog)", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Daemon runtime checkpoint projection must publish typed behavior-state rows from managed catalog payloads.");
|
||||
}
|
||||
|
||||
if (!unityProject.Contains("AetheriaRuntimeBehaviorStateProjector.cs", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Unity-compatible shared runtime builds must include the typed behavior-state projector.");
|
||||
}
|
||||
|
||||
var requiredWeaponStateCoverage = new[]
|
||||
|
|
|
|||
|
|
@ -4138,6 +4138,121 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.IsTrue(result.Intents.SensorPingRequested);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BehaviorStateProjectorDerivesEquipmentRowsFromCatalogPayloads()
|
||||
{
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[]
|
||||
{
|
||||
CatalogItem(
|
||||
"test-relay",
|
||||
new[]
|
||||
{
|
||||
new AetheriaRuntimeBehaviorPayload(
|
||||
3,
|
||||
"SensorRelay",
|
||||
0,
|
||||
Array.Empty<AetheriaRuntimeBehaviorField>()),
|
||||
new AetheriaRuntimeBehaviorPayload(
|
||||
4,
|
||||
"Shield",
|
||||
1,
|
||||
Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
})
|
||||
},
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
var equipment = new[]
|
||||
{
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = "test-relay",
|
||||
Enabled = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var states = AetheriaRuntimeBehaviorStateProjector.CreateEquipmentBehaviorStates(equipment, catalog);
|
||||
|
||||
Assert.AreEqual(2, states.Length);
|
||||
Assert.AreEqual(AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, states[0].OwnerKind);
|
||||
Assert.AreEqual(0, states[0].OwnerIndex);
|
||||
Assert.AreEqual(0, states[0].BehaviorIndex);
|
||||
Assert.AreEqual("SensorRelay", states[0].BehaviorKind);
|
||||
Assert.AreEqual(AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, states[1].OwnerKind);
|
||||
Assert.AreEqual(0, states[1].OwnerIndex);
|
||||
Assert.AreEqual(1, states[1].BehaviorIndex);
|
||||
Assert.AreEqual("Shield", states[1].BehaviorKind);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DaemonOperationsAcceptsBehaviorIntentFromCatalogProjectedState()
|
||||
{
|
||||
var run = RunWithTwoEntities();
|
||||
var actor = run.Zones[0].Entities[0];
|
||||
actor.BehaviorStates = new[]
|
||||
{
|
||||
new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
||||
OwnerIndex = 0,
|
||||
BehaviorIndex = 1,
|
||||
BehaviorKind = "StaleLocalBehavior"
|
||||
}
|
||||
};
|
||||
actor.Equipment = new[]
|
||||
{
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = "test-relay",
|
||||
Enabled = true
|
||||
}
|
||||
}
|
||||
};
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[]
|
||||
{
|
||||
CatalogItem(
|
||||
"test-relay",
|
||||
new[]
|
||||
{
|
||||
new AetheriaRuntimeBehaviorPayload(
|
||||
7,
|
||||
"SensorRelay",
|
||||
0,
|
||||
Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
})
|
||||
},
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(actor, catalog);
|
||||
Assert.AreEqual(1, actor.BehaviorStates.Count);
|
||||
Assert.AreEqual("SensorRelay", actor.BehaviorStates[0].BehaviorKind);
|
||||
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
||||
AetheriaRuntimeDaemonCommandKinds.SetBehaviorActive,
|
||||
"codex",
|
||||
"session-intents",
|
||||
36,
|
||||
"zone.0.entity.0");
|
||||
command.EquipmentIndex = 0;
|
||||
command.BehaviorIndex = 0;
|
||||
command.ScalarValue = 1.0;
|
||||
|
||||
var result = AetheriaRuntimeDaemonOperations.Execute(run, new[] { command });
|
||||
|
||||
Assert.AreEqual(1, result.AppliedCommandIds.Count);
|
||||
Assert.AreEqual(0, result.RejectedCommandIds.Count);
|
||||
Assert.AreEqual(1, result.Intents.Behaviors.Count);
|
||||
Assert.AreEqual("zone.0.entity.0", result.Intents.Behaviors[0].ActorEntityKey);
|
||||
Assert.AreEqual(0, result.Intents.Behaviors[0].EquipmentIndex);
|
||||
Assert.AreEqual(0, result.Intents.Behaviors[0].BehaviorIndex);
|
||||
Assert.IsTrue(result.Intents.Behaviors[0].Active);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DaemonOperationsRejectsWeaponGroupIntentForMissingGroup()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeAssemblyInfo.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeBehaviorMetadata.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeBehaviorStateProjector.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeAssets.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateBoundary.cs" />
|
||||
<Compile Include="Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeTradeItemDetailsSurfaceBuilder.cs" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeBehaviorStateProjector
|
||||
{
|
||||
public const string EquipmentOwnerKind = "equipment";
|
||||
|
||||
public static void EnsureEquipmentBehaviorStates(
|
||||
AetheriaRuntimeEntitySnapshotCommit? entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (entity == null)
|
||||
return;
|
||||
|
||||
var existing = entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>();
|
||||
var projected = CreateEquipmentBehaviorStates(entity.Equipment, catalog);
|
||||
if (projected.Length == 0)
|
||||
{
|
||||
entity.BehaviorStates = existing
|
||||
.Where(state => state != null && !string.Equals(state.OwnerKind, EquipmentOwnerKind, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
return;
|
||||
}
|
||||
|
||||
var existingByKey = existing
|
||||
.Where(state => state != null)
|
||||
.GroupBy(StateKey, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);
|
||||
var nonEquipmentStates = existing
|
||||
.Where(state => state != null && !string.Equals(state.OwnerKind, EquipmentOwnerKind, StringComparison.Ordinal));
|
||||
var equipmentStates = projected
|
||||
.Select(state => existingByKey.TryGetValue(StateKey(state), out var current) ? current : state);
|
||||
entity.BehaviorStates = nonEquipmentStates
|
||||
.Concat(equipmentStates)
|
||||
.OrderBy(state => state.OwnerKind, StringComparer.Ordinal)
|
||||
.ThenBy(state => state.OwnerIndex)
|
||||
.ThenBy(state => state.BehaviorIndex)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeBehaviorStateCommit[] CreateEquipmentBehaviorStates(
|
||||
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotCommit>? equipment,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
catalog ??= new AetheriaRuntimeCatalogSnapshot();
|
||||
return (equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.SelectMany((slot, equipmentIndex) => CreateEquipmentBehaviorStates(slot?.Item, equipmentIndex, catalog))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<AetheriaRuntimeBehaviorStateCommit> CreateEquipmentBehaviorStates(
|
||||
AetheriaRuntimeLoadoutItemCommit? item,
|
||||
int equipmentIndex,
|
||||
AetheriaRuntimeCatalogSnapshot catalog)
|
||||
{
|
||||
if (item == null || equipmentIndex < 0)
|
||||
yield break;
|
||||
|
||||
var typedItem = catalog.FindItem(item.ItemKey ?? "");
|
||||
var payloads = typedItem?.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>();
|
||||
for (var behaviorIndex = 0; behaviorIndex < payloads.Count; behaviorIndex++)
|
||||
{
|
||||
var payload = payloads[behaviorIndex];
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.Kind))
|
||||
continue;
|
||||
|
||||
yield return new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = EquipmentOwnerKind,
|
||||
OwnerIndex = equipmentIndex,
|
||||
BehaviorIndex = behaviorIndex,
|
||||
BehaviorKind = payload.Kind
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static string StateKey(AetheriaRuntimeBehaviorStateCommit? state)
|
||||
{
|
||||
if (state == null)
|
||||
return "";
|
||||
|
||||
return string.Join(
|
||||
"/",
|
||||
state.OwnerKind ?? "",
|
||||
state.OwnerIndex.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
state.BehaviorIndex.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||
state.BehaviorKind ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue