Publish loadout generation provenance
This commit is contained in:
parent
fb88740748
commit
2edf34916d
9 changed files with 143 additions and 7 deletions
|
|
@ -324,6 +324,11 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
Require(first.HullItemKey == second.HullItemKey &&
|
||||
first.Equipment.Select(value => value.ItemKey).SequenceEqual(second.Equipment.Select(value => value.ItemKey)),
|
||||
"same seed, map, faction and catalog must produce the same loadout");
|
||||
Require(first.Receipt.Seed == 42 && first.Receipt.SourceZoneIndex == 0 &&
|
||||
first.Receipt.AvailabilityFactionKey == "forge" &&
|
||||
first.Receipt.Selections.Any(value => value.Role == "hull" && value.ItemKey == "available-hull" &&
|
||||
value.ManufacturerKey == "forge" && value.ManufacturerDistance == 1 && value.Allegiance == 1),
|
||||
"generated loadouts must carry daemon-authored source and selection provenance");
|
||||
Require(station.Equipment.Any(value => value.ItemKey == "docking-bay") &&
|
||||
station.Equipment.Any(value => value.ItemKey == "cargo-bay") &&
|
||||
station.Equipment.Any(value => value.ItemKey == "capacitor") &&
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
{
|
||||
private readonly AetheriaRuntimeCatalogSnapshot _catalog;
|
||||
private readonly Random _random;
|
||||
private readonly uint _seed;
|
||||
private readonly int _zoneIndex;
|
||||
private readonly IReadOnlyDictionary<string, int> _homeZones;
|
||||
private readonly IReadOnlyDictionary<int, IReadOnlyList<int>> _adjacency;
|
||||
|
|
@ -20,7 +21,8 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
double priceExponent = 0.5)
|
||||
{
|
||||
_catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
|
||||
_random = new Random(seed == 0 ? 1u : seed);
|
||||
_seed = seed == 0 ? 1u : seed;
|
||||
_random = new Random(_seed);
|
||||
_zoneIndex = zoneIndex;
|
||||
_homeZones = homeZones ?? throw new ArgumentNullException(nameof(homeZones));
|
||||
_adjacency = adjacency ?? throw new ArgumentNullException(nameof(adjacency));
|
||||
|
|
@ -77,7 +79,37 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
|
||||
var cargo = PackCargo(cargoBay, availabilityFactionKey, scenarioCargo,
|
||||
string.Equals(entityKind, "station", StringComparison.OrdinalIgnoreCase));
|
||||
return new AetheriaDaemonLoadout(hull.ItemKey, slots.ToArray(), cargo);
|
||||
var selectedKeys = new[] { (Role: "hull", Key: hull.ItemKey) }
|
||||
.Concat(slots.Select(value => (Role: "equipment", Key: value.ItemKey)))
|
||||
.Concat(cargo.Select(value => (Role: "cargo", Key: value.Item.ItemKey)));
|
||||
var receipt = new AetheriaLoadoutGenerationReceipt
|
||||
{
|
||||
Seed = _seed,
|
||||
SourceZoneIndex = _zoneIndex,
|
||||
AvailabilityFactionKey = availabilityFactionKey,
|
||||
PriceExponent = _priceExponent,
|
||||
Selections = selectedKeys.Select(value => Selection(value.Role, value.Key, availabilityFactionKey)).ToArray()
|
||||
};
|
||||
return new AetheriaDaemonLoadout(hull.ItemKey, slots.ToArray(), cargo, receipt);
|
||||
}
|
||||
|
||||
private AetheriaLoadoutGenerationSelection Selection(string role, string itemKey, string factionKey)
|
||||
{
|
||||
var item = _catalog.FindItem(itemKey) ?? throw new InvalidOperationException($"Generated item {itemKey} left the catalog.");
|
||||
var faction = _catalog.FindCorporation(factionKey);
|
||||
var allegiance = string.Equals(item.ManufacturerKey, factionKey, StringComparison.Ordinal)
|
||||
? 1
|
||||
: (faction?.Allegiances ?? Array.Empty<AetheriaRuntimeCorporationAllegiance>())
|
||||
.FirstOrDefault(value => value.CorporationKey == item.ManufacturerKey)?.Weight ?? 0;
|
||||
return new AetheriaLoadoutGenerationSelection
|
||||
{
|
||||
Role = role,
|
||||
ItemKey = item.ItemKey,
|
||||
ManufacturerKey = item.ManufacturerKey,
|
||||
Price = item.Price,
|
||||
ManufacturerDistance = _homeZones.TryGetValue(item.ManufacturerKey, out var home) ? Distance(_zoneIndex, home) : 1,
|
||||
Allegiance = allegiance
|
||||
};
|
||||
}
|
||||
|
||||
private AetheriaRuntimeCatalogItem AddFreeSpaceItem(
|
||||
|
|
@ -314,4 +346,5 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
public sealed record AetheriaDaemonLoadout(
|
||||
string HullItemKey,
|
||||
AetheriaEntityItemSlot[] Equipment,
|
||||
AetheriaLoadoutItemSlot[] Cargo);
|
||||
AetheriaLoadoutItemSlot[] Cargo,
|
||||
AetheriaLoadoutGenerationReceipt Receipt);
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
Velocity = Vec2(vx, vy),
|
||||
FactionKey = faction,
|
||||
HullItemKey = loadout.HullItemKey,
|
||||
LoadoutGeneration = loadout.Receipt,
|
||||
IsActive = true,
|
||||
HeatsinksEnabled = true,
|
||||
TractorPower = 0,
|
||||
|
|
|
|||
|
|
@ -2598,7 +2598,24 @@ static AetheriaRuntimeEntitySnapshotCommit ToRuntimeEntity(
|
|||
Contacts = (entity.Contacts ?? Array.Empty<AetheriaEntityContactSnapshot>())
|
||||
.Select(contact => ToRuntimeContact(contact, entityIndices))
|
||||
.Where(contact => contact.TargetEntityIndex >= 0)
|
||||
.ToArray()
|
||||
.ToArray(),
|
||||
LoadoutGeneration = entity.LoadoutGeneration == null ? null : new AetheriaRuntimeLoadoutGenerationReceiptCommit
|
||||
{
|
||||
Seed = entity.LoadoutGeneration.Seed,
|
||||
SourceZoneIndex = entity.LoadoutGeneration.SourceZoneIndex,
|
||||
AvailabilityFactionKey = entity.LoadoutGeneration.AvailabilityFactionKey ?? "",
|
||||
PriceExponent = entity.LoadoutGeneration.PriceExponent,
|
||||
Selections = (entity.LoadoutGeneration.Selections ?? Array.Empty<AetheriaLoadoutGenerationSelection>())
|
||||
.Select(value => new AetheriaRuntimeLoadoutGenerationSelectionCommit
|
||||
{
|
||||
Role = value.Role ?? "",
|
||||
ItemKey = value.ItemKey ?? "",
|
||||
ManufacturerKey = value.ManufacturerKey ?? "",
|
||||
Price = value.Price,
|
||||
ManufacturerDistance = value.ManufacturerDistance,
|
||||
Allegiance = value.Allegiance
|
||||
}).ToArray()
|
||||
}
|
||||
};
|
||||
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(runtimeEntity, catalog);
|
||||
return runtimeEntity;
|
||||
|
|
|
|||
|
|
@ -1244,7 +1244,9 @@ static void RequireDaemonPlayableRunGenerationAuthority(string root)
|
|||
"FitsHardpoint(candidate, hardpoint)",
|
||||
"HasBehavior(candidate, controllerKind)",
|
||||
"for (var draw = 0; draw < 16 && pool.Count > 0; draw++)",
|
||||
"FindFit(interiorCells, width, height, item, occupied)"
|
||||
"FindFit(interiorCells, width, height, item, occupied)",
|
||||
"AetheriaLoadoutGenerationReceipt",
|
||||
"Selections = selectedKeys.Select"
|
||||
};
|
||||
|
||||
var daemonText = daemonProgram + "\n" + daemonZoneGenerator + "\n" + daemonLoadoutGenerator;
|
||||
|
|
|
|||
|
|
@ -607,6 +607,30 @@ public sealed class AetheriaEntitySnapshot
|
|||
|
||||
[Key(31)]
|
||||
public string[] AgentTaskCapabilities { get; set; } = [];
|
||||
|
||||
[Key(32)]
|
||||
public AetheriaLoadoutGenerationReceipt? LoadoutGeneration { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaLoadoutGenerationReceipt
|
||||
{
|
||||
[Key(0)] public uint Seed { get; set; }
|
||||
[Key(1)] public int SourceZoneIndex { get; set; }
|
||||
[Key(2)] public string AvailabilityFactionKey { get; set; } = "";
|
||||
[Key(3)] public double PriceExponent { get; set; }
|
||||
[Key(4)] public AetheriaLoadoutGenerationSelection[] Selections { get; set; } = [];
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaLoadoutGenerationSelection
|
||||
{
|
||||
[Key(0)] public string Role { get; set; } = "";
|
||||
[Key(1)] public string ItemKey { get; set; } = "";
|
||||
[Key(2)] public string ManufacturerKey { get; set; } = "";
|
||||
[Key(3)] public int Price { get; set; }
|
||||
[Key(4)] public int ManufacturerDistance { get; set; }
|
||||
[Key(5)] public double Allegiance { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
|
|||
|
|
@ -638,12 +638,42 @@ namespace GameCult.Aetheria.State.Verse
|
|||
["targetCommand"] = CommandName(AetheriaRuntimeDaemonCommandKinds.SetTarget),
|
||||
["actionCommand"] = CommandName(AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup)
|
||||
};
|
||||
if (entity.LoadoutGeneration != null)
|
||||
{
|
||||
props["loadoutGenerationSeed"] = entity.LoadoutGeneration.Seed.ToString(CultureInfo.InvariantCulture);
|
||||
props["loadoutGenerationZone"] = entity.LoadoutGeneration.SourceZoneIndex.ToString(CultureInfo.InvariantCulture);
|
||||
props["loadoutAvailabilityFaction"] = entity.LoadoutGeneration.AvailabilityFactionKey ?? "";
|
||||
props["loadoutPriceExponent"] = entity.LoadoutGeneration.PriceExponent.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeSurfaceComponent(
|
||||
$"aetheria.daemon.game.world.entity.{entity.EntityIndex}",
|
||||
"world.entity3d",
|
||||
props,
|
||||
Array.Empty<AetheriaRuntimeSurfaceComponent>());
|
||||
LoadoutGenerationItems(entity));
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent[] LoadoutGenerationItems(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity)
|
||||
{
|
||||
return (entity.LoadoutGeneration?.Selections ?? Array.Empty<AetheriaRuntimeLoadoutGenerationSelectionCommit>())
|
||||
.Select((value, index) => new AetheriaRuntimeSurfaceComponent(
|
||||
$"aetheria.daemon.game.world.entity.{entity.EntityIndex}.loadout.{index}",
|
||||
"loadout.item",
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["label"] = value.ItemKey ?? "",
|
||||
["status"] = value.Role ?? "",
|
||||
["detail"] = $"{value.ManufacturerKey} | {value.Price} credits | distance {value.ManufacturerDistance}",
|
||||
["badges"] = $"allegiance:{value.Allegiance:0.###}",
|
||||
["itemKey"] = value.ItemKey ?? "",
|
||||
["manufacturerKey"] = value.ManufacturerKey ?? "",
|
||||
["price"] = value.Price.ToString(CultureInfo.InvariantCulture),
|
||||
["manufacturerDistance"] = value.ManufacturerDistance.ToString(CultureInfo.InvariantCulture),
|
||||
["allegiance"] = value.Allegiance.ToString("0.###", CultureInfo.InvariantCulture)
|
||||
},
|
||||
Array.Empty<AetheriaRuntimeSurfaceComponent>()))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeSurfaceComponent PlayableWorldProjectile(
|
||||
|
|
|
|||
|
|
@ -961,6 +961,30 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
[Key(43)]
|
||||
public string HomeEntityId { get; set; } = "";
|
||||
|
||||
[Key(44)]
|
||||
public AetheriaRuntimeLoadoutGenerationReceiptCommit? LoadoutGeneration { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeLoadoutGenerationReceiptCommit
|
||||
{
|
||||
[Key(0)] public uint Seed { get; set; }
|
||||
[Key(1)] public int SourceZoneIndex { get; set; }
|
||||
[Key(2)] public string AvailabilityFactionKey { get; set; } = "";
|
||||
[Key(3)] public double PriceExponent { get; set; }
|
||||
[Key(4)] public IReadOnlyList<AetheriaRuntimeLoadoutGenerationSelectionCommit> Selections { get; set; } = Array.Empty<AetheriaRuntimeLoadoutGenerationSelectionCommit>();
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaRuntimeLoadoutGenerationSelectionCommit
|
||||
{
|
||||
[Key(0)] public string Role { get; set; } = "";
|
||||
[Key(1)] public string ItemKey { get; set; } = "";
|
||||
[Key(2)] public string ManufacturerKey { get; set; } = "";
|
||||
[Key(3)] public int Price { get; set; }
|
||||
[Key(4)] public int ManufacturerDistance { get; set; }
|
||||
[Key(5)] public double Allegiance { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
|---|---|---|---|---|---|---|
|
||||
| Run/session state | `ServerShared`, `ActionGameManager` | Aetheria daemon/CultCache | provider/session advertisement | all clients | owner-exists; wired; projected; not proven | restart and multi-client convergence |
|
||||
| Entity identity/loadout | `ServerShared/Entity*` | daemon | typed entity and inventory surfaces | EveUnity/Electron | owner-exists; wired; projected; not proven | loadout round trip and reconnect |
|
||||
| Loadout generation | map/faction availability, item cost, hull hardpoints and role constraints | daemon using canonical galaxy, faction knowledge/economy and typed catalog | generated loadout plus provenance/availability facts | Eve inspection and refit surfaces | daemon now uses the fossil candidate filter and seeded weighted selection: manufacturer must be present and allied, ally weight decays by home-zone graph distance, price uses the authored exponent, hardpoints require exact type/cell/rotation fit and controller role, and cargo/capacitor plus station docking equipment must fit remaining hull space; stations draw up to sixteen faction-available equipment rows and pack largest-first into the selected cargo bay's actual interior shape; Terminus explicitly maps combat-role factions to catalog availability corporations | multi-zone generated-world parity, faction-local stream/call-order parity, and generation provenance projection |
|
||||
| Loadout generation | map/faction availability, item cost, hull hardpoints and role constraints | daemon using canonical galaxy, faction knowledge/economy and typed catalog | generated loadout plus provenance/availability facts | Eve inspection and refit surfaces | daemon now uses the fossil candidate filter and seeded weighted selection: manufacturer must be present and allied, ally weight decays by home-zone graph distance, price uses the authored exponent, hardpoints require exact type/cell/rotation fit and controller role, and cargo/capacitor plus station docking equipment must fit remaining hull space; stations draw up to sixteen faction-available equipment rows and pack largest-first into the selected cargo bay's actual interior shape; every generated entity persists an immutable source/selection receipt that reaches Eve as `loadout.item` semantics; Terminus explicitly maps combat-role factions to catalog availability corporations | multi-zone generated-world parity and exact faction-local stream/call-order parity |
|
||||
| Flight intent | `Ship`, `Thruster`, `AetherDrive`, input paths | daemon intent + Ymir physics | analog movement/look/tractor | EveUnity input driver | movement intents feed Ymir-owned body integration; authored thrust allocation, torque, energy and heat remain simplified | prove original drive/equipment behavior ordering and runtime interpolation |
|
||||
| Collision | Unity physics and entity handlers | Ymir | collision/result facts | visual interpolation only | Ymir.Core world stepping wired for entity bodies, radial fields and contacts; missing-owner negative proof passes | publish authoritative collision events and prove Unity cannot write transforms back |
|
||||
| Targeting/contacts | sensors + `ActionGameManager` | daemon | contact/target semantics | target HUD and indicators | simplified owner wired/projected; daemon weapon lock now acquires and decays from facing, elapsed time, and observer-local contact confidence instead of granting an immediate perfect lock | target-switch reset, angular decay, visibility/nearest/explicit cases, and runtime lock feedback |
|
||||
|
|
|
|||
Loading…
Reference in a new issue