Add daemon-owned dockyard station variant
This commit is contained in:
parent
6c94ccfd6a
commit
0b5284f5af
10 changed files with 438 additions and 15 deletions
|
|
@ -93,7 +93,11 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
|
||||
public void RunCombatLock() => RunCheck(InstantWeaponRequestSurvivesLockAcquisition);
|
||||
|
||||
public void RunLoadout() => DaemonLoadoutsRespectFactionAvailabilityAndHullRoles();
|
||||
public void RunLoadout()
|
||||
{
|
||||
DaemonNativeDockyardMaterializesTypedCatalog();
|
||||
DaemonLoadoutsRespectFactionAvailabilityAndHullRoles();
|
||||
}
|
||||
|
||||
public void RunPickup()
|
||||
{
|
||||
|
|
@ -1761,6 +1765,10 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
private static void InputCapabilityPublishesNativeTargetAndInteractControls()
|
||||
{
|
||||
var actor = Entity(0, 0, "player");
|
||||
actor.VelocityX = 12;
|
||||
actor.VelocityY = -7;
|
||||
actor.HelmStrafe = 0.5;
|
||||
actor.HelmForward = 1;
|
||||
var near = Entity(1, 0, "enemy");
|
||||
near.PositionX = 4;
|
||||
var middle = Entity(2, 0, "enemy");
|
||||
|
|
@ -2553,6 +2561,14 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
var disarmed = actor.WeaponStates.Single();
|
||||
RequireEqual(0, actor.ActiveWeaponGroups.Count,
|
||||
"docking must clear the daemon-owned held weapon-group latch");
|
||||
RequireNear(0, actor.VelocityX, 0.000001,
|
||||
"docking must clear retained X velocity before Ymir excludes the attached child body");
|
||||
RequireNear(0, actor.VelocityY, 0.000001,
|
||||
"docking must clear retained Z velocity before Ymir excludes the attached child body");
|
||||
RequireNear(0, actor.HelmStrafe, 0.000001,
|
||||
"docking must clear retained strafe intent");
|
||||
RequireNear(0, actor.HelmForward, 0.000001,
|
||||
"docking must clear retained forward intent");
|
||||
Require(!disarmed.Firing && !disarmed.TriggerPending && disarmed.BurstRemaining == 0 &&
|
||||
!disarmed.Charging && !disarmed.Charged && disarmed.Charge == 0 &&
|
||||
disarmed.LockTargetEntityIndex < 0 && disarmed.LockProgress == 0,
|
||||
|
|
@ -5522,7 +5538,23 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
cargo.InteriorShapeCells = Enumerable.Range(0, 3).SelectMany(y => Enumerable.Range(0, 3)
|
||||
.Select(x => new AetheriaRuntimeShapeCell(x, y))).ToArray();
|
||||
var docking = Item("docking-bay", AetheriaRuntimeItemCategories.DockingBay, "forge", 35);
|
||||
docking.HardpointType = "Internal";
|
||||
docking.HardpointType = "Tool";
|
||||
var dockyardBay = Item("dockyard-bay", AetheriaRuntimeItemCategories.DockingBay, "forge", 70, "DockingBay");
|
||||
var dockyardHull = Item("dockyard-hull", AetheriaRuntimeItemCategories.Hull, "forge", 600);
|
||||
dockyardHull.HardpointType = "Hull";
|
||||
dockyardHull.HullType = "Station";
|
||||
dockyardHull.ShapeWidth = 6;
|
||||
dockyardHull.ShapeHeight = 4;
|
||||
dockyardHull.ShapeCells = Enumerable.Range(0, 4).SelectMany(y => Enumerable.Range(0, 6)
|
||||
.Select(x => new AetheriaRuntimeShapeCell(x, y))).ToArray();
|
||||
dockyardHull.OccupiedCells = dockyardHull.ShapeCells.Count;
|
||||
dockyardHull.Hardpoints = Enumerable.Range(0, 6)
|
||||
.SelectMany(x => new[]
|
||||
{
|
||||
new AetheriaRuntimeHardpoint("DockingBay", x, 0, 1, 1, 1, one, "", "None", 0),
|
||||
new AetheriaRuntimeHardpoint("DockingBay", x, 3, 1, 1, 1, one, "", "None", 0)
|
||||
})
|
||||
.ToArray();
|
||||
var capacitor = Item("capacitor", AetheriaRuntimeItemCategories.Gear, "forge", 25, "", "Capacitor");
|
||||
capacitor.HardpointType = "Internal";
|
||||
var faction = new AetheriaRuntimeCorporation("forge", "Forge", "F", "", "", "", 1, 1,
|
||||
|
|
@ -5532,7 +5564,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
var reactor = Item("reactor", AetheriaRuntimeItemCategories.Gear, "forge", 25, "", "Reactor");
|
||||
reactor.HardpointType = "Internal";
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
[availableHull, unavailableHull, stationHull, cockpit, wrongController, weapon, shipSensor, stationSensor, cargo, docking, capacitor, reactor],
|
||||
[availableHull, unavailableHull, stationHull, dockyardHull, cockpit, wrongController, weapon, shipSensor, stationSensor, cargo, docking, dockyardBay, capacitor, reactor],
|
||||
[faction, foreign], Array.Empty<AetheriaRuntimeNameFile>());
|
||||
var fallbackCatalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
[availableHull, unavailableHull, stationHull, cockpit, wrongController, weapon, shipSensor, cargo, docking, capacitor, reactor],
|
||||
|
|
@ -5555,9 +5587,12 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
|
||||
var first = new AetheriaDaemonLoadoutGenerator(catalog, 42, 0, homes, adjacency).Build("ship", "forge");
|
||||
var second = new AetheriaDaemonLoadoutGenerator(catalog, 42, 0, homes, adjacency).Build("ship", "forge");
|
||||
var station = new AetheriaDaemonLoadoutGenerator(catalog, 84, 0, homes, adjacency).Build("station", "forge");
|
||||
var station = new AetheriaDaemonLoadoutGenerator(catalog, 84, 0, homes, adjacency)
|
||||
.Build("station", "forge", "station-hull");
|
||||
var fallbackStation = new AetheriaDaemonLoadoutGenerator(fallbackCatalog, 84, 0, homes, adjacency)
|
||||
.Build("station", "forge");
|
||||
.Build("station", "forge", "station-hull");
|
||||
var dockyard = new AetheriaDaemonLoadoutGenerator(catalog, 85, 0, homes, adjacency)
|
||||
.Build("station", "forge", "dockyard-hull");
|
||||
var uninterruptedForge = new AetheriaDaemonLoadoutGenerator(catalog, 101, 0, homes, adjacency);
|
||||
var interleavedForge = new AetheriaDaemonLoadoutGenerator(catalog, 101, 0, homes, adjacency);
|
||||
var foreignStream = new AetheriaDaemonLoadoutGenerator(catalog, 202, 1, homes, adjacency);
|
||||
|
|
@ -5609,6 +5644,14 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
"station generation must fill a large sensor hardpoint with the largest available compatible array");
|
||||
Require(fallbackStation.Equipment.Any(value => value.ItemKey == "ship-sensor"),
|
||||
"a smaller sensor must remain a valid fallback when it fits inside a larger same-type hardpoint");
|
||||
Require(fallbackStation.DockingBays.Single().ItemKey == "docking-bay",
|
||||
"a station without dedicated docking mounts must retain the canonical non-dedicated free-space bay path");
|
||||
RequireEqual("dockyard-hull", dockyard.HullItemKey,
|
||||
"an authored station variant must be selectable without bypassing hull generation");
|
||||
RequireEqual(12, dockyard.DockingBays.Length,
|
||||
"the dockyard hull must fill every authored docking hardpoint through normal loadout generation");
|
||||
Require(dockyard.DockingBays.All(value => value.ItemKey == "dockyard-bay"),
|
||||
"dedicated docking hardpoints must only accept the matching docking-bay equipment type");
|
||||
Require(station.Cargo.Length > 0 && station.Cargo.Length <= cargo.InteriorOccupiedCells &&
|
||||
station.Cargo.All(value => value.Item.ItemKey != "cheap-foreign-hull" &&
|
||||
catalog.FindItem(value.Item.ItemKey) != null) &&
|
||||
|
|
@ -5766,6 +5809,89 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
additionalItems.Concat([hull, thruster]).ToArray(), [], []);
|
||||
}
|
||||
|
||||
private static void DaemonNativeDockyardMaterializesTypedCatalog()
|
||||
{
|
||||
Require(AetheriaDocumentRegistry.DocumentTypes.Contains(
|
||||
typeof(GameCult.Eve.PluginFields.EveFieldsSplatsDocument)),
|
||||
"Aetheria's public registry must include every typed Eve Fields document the daemon persists");
|
||||
const string zenithId = "82efc0a5-1ba5-4ff3-a281-b2e6e247521d";
|
||||
const string mediumBayId = "3e930a2c-ac72-4385-98aa-1c5b0b90db46";
|
||||
var root = Path.Combine(Path.GetTempPath(), $"aetheria-dockyard-catalog-{Guid.NewGuid():N}");
|
||||
var statePath = Path.Combine(root, "aetheria.cc");
|
||||
Directory.CreateDirectory(root);
|
||||
try
|
||||
{
|
||||
using var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult();
|
||||
node.MutableDocument<AetheriaItemDefinition>(AetheriaCatalogKeys.ItemDefinitionFromLegacyId(zenithId))
|
||||
.ReplaceAsync(new AetheriaItemDefinition
|
||||
{
|
||||
Name = "Zenith",
|
||||
LegacyId = zenithId,
|
||||
Category = AetheriaRuntimeItemCategories.Hull,
|
||||
HullType = "Station",
|
||||
HardpointType = "Hull",
|
||||
ManufacturerLegacyId = "11111111-1111-1111-1111-111111111111",
|
||||
Price = 100,
|
||||
Mass = 100,
|
||||
ShapeWidth = 12,
|
||||
ShapeHeight = 12,
|
||||
ShapeCells = Enumerable.Range(0, 12).SelectMany(y => Enumerable.Range(0, 12)
|
||||
.Select(x => new AetheriaShapeCell { X = x, Y = y })).ToArray(),
|
||||
HullPrefab = "Assets/Prefabs/Zenith.prefab",
|
||||
HullArmor = 4,
|
||||
HullDrag = 2
|
||||
}).GetAwaiter().GetResult();
|
||||
node.MutableDocument<AetheriaItemDefinition>(AetheriaCatalogKeys.ItemDefinitionFromLegacyId(mediumBayId))
|
||||
.ReplaceAsync(new AetheriaItemDefinition
|
||||
{
|
||||
Name = "Medium Docking Bay",
|
||||
LegacyId = mediumBayId,
|
||||
Category = AetheriaRuntimeItemCategories.DockingBay,
|
||||
HardpointType = "Tool",
|
||||
ManufacturerLegacyId = "11111111-1111-1111-1111-111111111111",
|
||||
Price = 20,
|
||||
ShapeWidth = 4,
|
||||
ShapeHeight = 4,
|
||||
OccupiedCells = 16,
|
||||
ShapeCells = Enumerable.Range(0, 4).SelectMany(y => Enumerable.Range(0, 4)
|
||||
.Select(x => new AetheriaShapeCell { X = x, Y = y })).ToArray(),
|
||||
DockingMaxSizeX = 12,
|
||||
DockingMaxSizeY = 12
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
AetheriaDaemonNativeCatalog.EnsureAsync(node).GetAwaiter().GetResult();
|
||||
node.FlushAsync().GetAwaiter().GetResult();
|
||||
var catalog = node.RuntimeCatalog().Latest();
|
||||
var hull = catalog.FindItem(AetheriaDaemonNativeCatalog.DockyardHullItemKey);
|
||||
var bay = catalog.FindItem(AetheriaDaemonNativeCatalog.DockyardBayItemKey);
|
||||
Require(hull != null && hull.HullType == "Station" && hull.ShapeWidth == 24 && hull.ShapeHeight == 24,
|
||||
"daemon-native station content must materialize as a distinct typed hull definition");
|
||||
RequireEqual(12, hull!.Hardpoints.Count(hardpoint => hardpoint.Type == "DockingBay"),
|
||||
"dockyard capacity must be authored by twelve docking hardpoints on the hull");
|
||||
Require(bay != null && bay.Category == AetheriaRuntimeItemCategories.DockingBay &&
|
||||
bay.HardpointType == "DockingBay" && bay.DockingMaxSizeX == 12 && bay.DockingMaxSizeY == 12,
|
||||
"dockyard berths must remain typed docking-bay equipment with the source bay's ship-size contract");
|
||||
|
||||
var dockyardEntity = Entity(0, 0, "neutral");
|
||||
dockyardEntity.Kind = "station";
|
||||
dockyardEntity.HullItemKey = hull.ItemKey;
|
||||
var zenithItemKey = $"aetheria.item_definition:legacy:{zenithId}";
|
||||
var zenithAssetKey = AetheriaRuntimeAssets.HullPrefabAssetKey(zenithItemKey);
|
||||
RequireEqual(zenithAssetKey, AetheriaRuntimeAssets.ResolveEntityPrefabAssetRef(dockyardEntity, catalog),
|
||||
"daemon-native hull variants must explicitly reuse their provider-owned presentation hull asset");
|
||||
var manifest = AetheriaRuntimeAssets.ProjectManifest(catalog);
|
||||
Require(manifest.Assets.Any(asset => asset.Ref.AssetKey == zenithAssetKey),
|
||||
"provider manifest must advertise the canonical Zenith presentation asset used by the dockyard");
|
||||
Require(!manifest.Assets.Any(asset => asset.Ref.AssetKey ==
|
||||
AetheriaRuntimeAssets.HullPrefabAssetKey(hull.ItemKey)),
|
||||
"provider manifest must not invent an unbaked prefab for a hull with an explicit presentation alias");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeCatalogSnapshot EquipAetherDrive(
|
||||
IEnumerable<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
params AetheriaRuntimeCatalogItem[] additionalItems)
|
||||
|
|
|
|||
|
|
@ -35,12 +35,14 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
|
||||
public AetheriaDaemonLoadout Build(
|
||||
string entityKind,
|
||||
string availabilityFactionKey)
|
||||
string availabilityFactionKey,
|
||||
string hullItemKey = "")
|
||||
{
|
||||
var hullType = string.Equals(entityKind, "station", StringComparison.OrdinalIgnoreCase) ? "Station" : "Ship";
|
||||
var hull = Pick(availabilityFactionKey, 0, item =>
|
||||
string.Equals(item.Category, AetheriaRuntimeItemCategories.Hull, StringComparison.Ordinal) &&
|
||||
string.Equals(item.HullType, hullType, StringComparison.Ordinal))
|
||||
string.Equals(item.HullType, hullType, StringComparison.Ordinal) &&
|
||||
(string.IsNullOrWhiteSpace(hullItemKey) || string.Equals(item.ItemKey, hullItemKey, StringComparison.Ordinal)))
|
||||
?? throw new InvalidOperationException($"No available {hullType} hull for faction {availabilityFactionKey}.");
|
||||
|
||||
var occupied = new HashSet<(int X, int Y)>();
|
||||
|
|
@ -48,9 +50,12 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
foreach (var cell in hardpoint.ShapeCells ?? Array.Empty<AetheriaRuntimeShapeCell>())
|
||||
occupied.Add((hardpoint.PositionX + cell.X, hardpoint.PositionY + cell.Y));
|
||||
var slots = new List<AetheriaEntityItemSlot>();
|
||||
if (string.Equals(entityKind, "station", StringComparison.OrdinalIgnoreCase))
|
||||
var hasDedicatedDockingMounts = (hull.Hardpoints ?? Array.Empty<AetheriaRuntimeHardpoint>())
|
||||
.Any(hardpoint => string.Equals(hardpoint.Type, "DockingBay", StringComparison.Ordinal));
|
||||
if (string.Equals(entityKind, "station", StringComparison.OrdinalIgnoreCase) && !hasDedicatedDockingMounts)
|
||||
AddFreeSpaceItem(hull, availabilityFactionKey, occupied, slots, 2,
|
||||
item => string.Equals(item.Category, AetheriaRuntimeItemCategories.DockingBay, StringComparison.Ordinal));
|
||||
item => string.Equals(item.Category, AetheriaRuntimeItemCategories.DockingBay, StringComparison.Ordinal) &&
|
||||
!string.Equals(item.HardpointType, "DockingBay", StringComparison.Ordinal));
|
||||
|
||||
var previous = new List<AetheriaRuntimeCatalogItem>();
|
||||
foreach (var hardpoint in (hull.Hardpoints ?? Array.Empty<AetheriaRuntimeHardpoint>())
|
||||
|
|
@ -61,7 +66,7 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
: "";
|
||||
var item = previous.FirstOrDefault(candidate => FitsHardpoint(candidate, hardpoint));
|
||||
item ??= PickHardpoint(availabilityFactionKey, hardpoint, candidate =>
|
||||
IsGear(candidate) && FitsHardpoint(candidate, hardpoint) &&
|
||||
(IsGear(candidate) || IsDockingBay(candidate)) && FitsHardpoint(candidate, hardpoint) &&
|
||||
(controllerKind.Length == 0 || HasBehavior(candidate, controllerKind)));
|
||||
if (item == null)
|
||||
{
|
||||
|
|
@ -412,6 +417,9 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
(string.Equals(item.Category, AetheriaRuntimeItemCategories.CargoBay, StringComparison.Ordinal) ||
|
||||
string.Equals(item.Category, AetheriaRuntimeItemCategories.DockingBay, StringComparison.Ordinal));
|
||||
|
||||
private static bool IsDockingBay(AetheriaRuntimeCatalogItem item) =>
|
||||
string.Equals(item.Category, AetheriaRuntimeItemCategories.DockingBay, StringComparison.Ordinal);
|
||||
|
||||
private static AetheriaEntityItemSlot Slot(int x, int y, string itemKey, int rotation = 0) => new()
|
||||
{
|
||||
Position = new AetheriaGridCoord { X = x, Y = y }, ItemKey = itemKey,
|
||||
|
|
|
|||
196
Aetheria.State.Daemon/AetheriaDaemonNativeCatalog.cs
Normal file
196
Aetheria.State.Daemon/AetheriaDaemonNativeCatalog.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
using Aetheria.State;
|
||||
using Aetheria.State.Documents;
|
||||
|
||||
internal static class AetheriaDaemonNativeCatalog
|
||||
{
|
||||
private const string ZenithHullLegacyId = "82efc0a5-1ba5-4ff3-a281-b2e6e247521d";
|
||||
private const string MediumDockingBayLegacyId = "3e930a2c-ac72-4385-98aa-1c5b0b90db46";
|
||||
private const string DockyardHullLegacyId = "ca098005-8cc8-47f4-be99-7bc842805359";
|
||||
private const string DockyardBayLegacyId = "8ec30f8d-8536-48b4-bd64-65f29f229895";
|
||||
|
||||
public static string DockyardHullItemKey => ItemKey(DockyardHullLegacyId);
|
||||
public static string DockyardBayItemKey => ItemKey(DockyardBayLegacyId);
|
||||
|
||||
public static async Task EnsureAsync(AetheriaStateNode node)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(node);
|
||||
|
||||
var zenith = await node.MutableDocument<AetheriaItemDefinition>(
|
||||
AetheriaCatalogKeys.ItemDefinitionFromLegacyId(ZenithHullLegacyId))
|
||||
.ReadAsync()
|
||||
.ConfigureAwait(false)
|
||||
?? throw new InvalidDataException("The daemon-native dockyard requires the canonical Zenith station hull.");
|
||||
var mediumBay = await node.MutableDocument<AetheriaItemDefinition>(
|
||||
AetheriaCatalogKeys.ItemDefinitionFromLegacyId(MediumDockingBayLegacyId))
|
||||
.ReadAsync()
|
||||
.ConfigureAwait(false)
|
||||
?? throw new InvalidDataException("The daemon-native dockyard requires the canonical medium docking bay.");
|
||||
|
||||
await node.MutableDocument<AetheriaItemDefinition>(
|
||||
AetheriaCatalogKeys.ItemDefinitionFromLegacyId(DockyardHullLegacyId))
|
||||
.ReplaceAsync(CreateDockyardHull(zenith))
|
||||
.ConfigureAwait(false);
|
||||
await node.MutableDocument<AetheriaItemDefinition>(
|
||||
AetheriaCatalogKeys.ItemDefinitionFromLegacyId(DockyardBayLegacyId))
|
||||
.ReplaceAsync(CreateDockyardBay(mediumBay))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static AetheriaItemDefinition CreateDockyardHull(AetheriaItemDefinition source)
|
||||
{
|
||||
const int width = 24;
|
||||
const int height = 24;
|
||||
var hardpoints = Enumerable.Range(0, 6)
|
||||
.SelectMany(index => new[]
|
||||
{
|
||||
Hardpoint("DockingBay", index * 4, 0, 4, 4, source.HullArmor),
|
||||
Hardpoint("DockingBay", index * 4, 20, 4, 4, source.HullArmor)
|
||||
})
|
||||
.Concat(new[]
|
||||
{
|
||||
Hardpoint("Reactor", 10, 10, 4, 4, source.HullArmor),
|
||||
Hardpoint("Radiator", 8, 10, 2, 2, source.HullArmor),
|
||||
Hardpoint("Radiator", 14, 10, 2, 2, source.HullArmor),
|
||||
Hardpoint("Radiator", 10, 14, 2, 2, source.HullArmor),
|
||||
Hardpoint("Radiator", 12, 14, 2, 2, source.HullArmor),
|
||||
Hardpoint("Sensors", 11, 16, 2, 2, source.HullArmor)
|
||||
})
|
||||
.ToArray();
|
||||
var hullCells = RectangleCells(0, 0, width, height);
|
||||
var interiorCells = RectangleCells(1, 1, width - 2, height - 2);
|
||||
|
||||
var hull = Copy(source, DockyardHullLegacyId, "Zenith Dockyard");
|
||||
hull.Description = "A high-capacity Zenith station variant built around twelve dedicated docking berths.";
|
||||
hull.Mass = source.Mass * 4.5;
|
||||
hull.Volume = hullCells.Length;
|
||||
hull.Price = checked(source.Price * 5);
|
||||
hull.ShapeWidth = width;
|
||||
hull.ShapeHeight = height;
|
||||
hull.OccupiedCells = hullCells.Length;
|
||||
hull.ShapeCells = hullCells;
|
||||
hull.InteriorShapeWidth = width;
|
||||
hull.InteriorShapeHeight = height;
|
||||
hull.InteriorOccupiedCells = interiorCells.Length;
|
||||
hull.InteriorShapeCells = interiorCells;
|
||||
hull.Hardpoints = hardpoints;
|
||||
hull.HullDrag = source.HullDrag * 1.75;
|
||||
hull.Tags = Tags(
|
||||
source.Tags,
|
||||
"origin:aetheria-daemon",
|
||||
"station-role:dockyard",
|
||||
"docking-capacity:12",
|
||||
$"presentation-hull-item:{ItemKey(ZenithHullLegacyId)}");
|
||||
return hull;
|
||||
}
|
||||
|
||||
private static AetheriaItemDefinition CreateDockyardBay(AetheriaItemDefinition source)
|
||||
{
|
||||
var bay = Copy(source, DockyardBayLegacyId, "Dockyard Berth");
|
||||
bay.Description = "A medium docking bay packaged for a dedicated station docking hardpoint.";
|
||||
bay.HardpointType = "DockingBay";
|
||||
bay.Price = checked(source.Price * 2);
|
||||
bay.Tags = Tags(source.Tags, "origin:aetheria-daemon", "station-role:dockyard");
|
||||
return bay;
|
||||
}
|
||||
|
||||
private static AetheriaItemDefinition Copy(AetheriaItemDefinition source, string legacyId, string name) => new()
|
||||
{
|
||||
Name = name,
|
||||
Category = source.Category,
|
||||
LegacyId = legacyId,
|
||||
Description = source.Description,
|
||||
Mass = source.Mass,
|
||||
Volume = source.Volume,
|
||||
Tags = (source.Tags ?? []).ToArray(),
|
||||
ManufacturerLegacyId = source.ManufacturerLegacyId,
|
||||
Price = source.Price,
|
||||
ShapeWidth = source.ShapeWidth,
|
||||
ShapeHeight = source.ShapeHeight,
|
||||
OccupiedCells = source.OccupiedCells,
|
||||
HardpointType = source.HardpointType,
|
||||
HullType = source.HullType,
|
||||
BehaviorKinds = (source.BehaviorKinds ?? []).ToArray(),
|
||||
BehaviorCount = source.BehaviorCount,
|
||||
MaxStack = source.MaxStack,
|
||||
Stackable = source.Stackable,
|
||||
Duration = source.Duration,
|
||||
Durability = source.Durability,
|
||||
WeaponRange = source.WeaponRange,
|
||||
WeaponCaliber = source.WeaponCaliber,
|
||||
WeaponType = source.WeaponType,
|
||||
WeaponFireTypes = source.WeaponFireTypes,
|
||||
WeaponModifiers = source.WeaponModifiers,
|
||||
ShapeCells = Clone(source.ShapeCells),
|
||||
InteriorShapeWidth = source.InteriorShapeWidth,
|
||||
InteriorShapeHeight = source.InteriorShapeHeight,
|
||||
InteriorOccupiedCells = source.InteriorOccupiedCells,
|
||||
InteriorShapeCells = Clone(source.InteriorShapeCells),
|
||||
Hardpoints = (source.Hardpoints ?? []).Select(Clone).ToArray(),
|
||||
BehaviorPayloads = source.BehaviorPayloads ?? [],
|
||||
MinimumTemperature = source.MinimumTemperature,
|
||||
MaximumTemperature = source.MaximumTemperature,
|
||||
ThermalPerformanceCurveKeys = source.ThermalPerformanceCurveKeys ?? [],
|
||||
HullPrefab = source.HullPrefab,
|
||||
SimpleCommodityCategory = source.SimpleCommodityCategory,
|
||||
CompoundCommodityCategory = source.CompoundCommodityCategory,
|
||||
SpecificHeat = source.SpecificHeat,
|
||||
Conductivity = source.Conductivity,
|
||||
HullGridOffset = source.HullGridOffset,
|
||||
HullArmor = source.HullArmor,
|
||||
HullDrag = source.HullDrag,
|
||||
HullCanTow = source.HullCanTow,
|
||||
DockingMaxSizeX = source.DockingMaxSizeX,
|
||||
DockingMaxSizeY = source.DockingMaxSizeY,
|
||||
ActionBarIcon = source.ActionBarIcon,
|
||||
ThermalResilience = source.ThermalResilience,
|
||||
AudioStats = source.AudioStats ?? [],
|
||||
EffectivenessCurveKeys = source.EffectivenessCurveKeys ?? []
|
||||
};
|
||||
|
||||
private static AetheriaItemHardpoint Clone(AetheriaItemHardpoint source) => new()
|
||||
{
|
||||
Type = source.Type,
|
||||
PositionX = source.PositionX,
|
||||
PositionY = source.PositionY,
|
||||
ShapeWidth = source.ShapeWidth,
|
||||
ShapeHeight = source.ShapeHeight,
|
||||
OccupiedCells = source.OccupiedCells,
|
||||
ShapeCells = Clone(source.ShapeCells),
|
||||
Transform = source.Transform,
|
||||
Rotation = source.Rotation,
|
||||
Armor = source.Armor
|
||||
};
|
||||
|
||||
private static AetheriaItemHardpoint Hardpoint(
|
||||
string type,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
double armor) => new()
|
||||
{
|
||||
Type = type,
|
||||
PositionX = x,
|
||||
PositionY = y,
|
||||
ShapeWidth = width,
|
||||
ShapeHeight = height,
|
||||
OccupiedCells = width * height,
|
||||
ShapeCells = RectangleCells(0, 0, width, height),
|
||||
Rotation = "None",
|
||||
Armor = armor
|
||||
};
|
||||
|
||||
private static AetheriaShapeCell[] RectangleCells(int x, int y, int width, int height) =>
|
||||
Enumerable.Range(y, height)
|
||||
.SelectMany(row => Enumerable.Range(x, width)
|
||||
.Select(column => new AetheriaShapeCell { X = column, Y = row }))
|
||||
.ToArray();
|
||||
|
||||
private static AetheriaShapeCell[] Clone(IEnumerable<AetheriaShapeCell>? cells) =>
|
||||
(cells ?? []).Select(cell => new AetheriaShapeCell { X = cell.X, Y = cell.Y }).ToArray();
|
||||
|
||||
private static string[] Tags(IEnumerable<string>? tags, params string[] added) =>
|
||||
(tags ?? []).Concat(added).Distinct(StringComparer.Ordinal).ToArray();
|
||||
|
||||
private static string ItemKey(string legacyId) => $"aetheria.item_definition:legacy:{legacyId}";
|
||||
}
|
||||
|
|
@ -249,7 +249,7 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
|
||||
var entities = new[]
|
||||
{
|
||||
Entity(loadouts, availabilityFactions, "Anchor Station", "station", -50, -30, 0, 0, "player", 760, keys[6], [keys[1], keys[2], keys[3], keys[4], keys[6], keys[7], keys[11]], 2, 900),
|
||||
Entity(loadouts, availabilityFactions, "Anchor Station", "station", -50, -30, 0, 0, "player", 760, keys[6], [keys[1], keys[2], keys[3], keys[4], keys[6], keys[7], keys[11]], 2, 900, AetheriaDaemonNativeCatalog.DockyardHullItemKey),
|
||||
Entity(loadouts, availabilityFactions, "Vanguard One", "ship", -40, -30, 0, 0, "player", 540, keys[6], [keys[0], keys[2], keys[4], keys[6], keys[7]]),
|
||||
Entity(loadouts, availabilityFactions, "Wing Two", "ship", 145, 125, -5, 7, "player", 450, keys[6], [keys[0], keys[1], keys[6], keys[8]]),
|
||||
Entity(loadouts, availabilityFactions, "Torch Three", "ship", -235, 210, 8, -4, "player", 500, keys[7], [keys[0], keys[1], keys[7]]),
|
||||
|
|
@ -277,6 +277,14 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
string scenario)
|
||||
{
|
||||
// Hold the proof pilot inside the station's fossil 25-unit docking envelope while
|
||||
// the released client negotiates its provider session. Ymir's stellar field owns
|
||||
// the trajectory; this is initial world state, not a client-side position lock.
|
||||
entities[1].Velocity = Vec2(-70, -47);
|
||||
var playerFactionKey = entities[1].FactionKey;
|
||||
for (var index = 0; index < entities.Length; index++)
|
||||
if (index != 6)
|
||||
entities[index].FactionKey = playerFactionKey;
|
||||
entities[1].CargoContents = [new AetheriaCargoBayLoadout()];
|
||||
entities[6].StatGrids =
|
||||
[
|
||||
|
|
@ -387,10 +395,11 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
string target,
|
||||
string[] contactKeys,
|
||||
int securityLevel = 0,
|
||||
double securityRadius = 0)
|
||||
double securityRadius = 0,
|
||||
string hullItemKey = "")
|
||||
{
|
||||
var availabilityFaction = availabilityFactions[faction];
|
||||
var loadout = loadouts[availabilityFaction].Build(kind, availabilityFaction);
|
||||
var loadout = loadouts[availabilityFaction].Build(kind, availabilityFaction, hullItemKey);
|
||||
var entity = AetheriaDaemonGeneratedEntityFactory.Create(
|
||||
name,
|
||||
kind,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ using var discoveryHost = new AetheriaVerseDiscoveryHost(node);
|
|||
|
||||
await EnsureWorldDocumentAsync(node).ConfigureAwait(false);
|
||||
await EnsureTradeValuePolicyAsync(node, startedAtUtc).ConfigureAwait(false);
|
||||
await AetheriaDaemonNativeCatalog.EnsureAsync(node).ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
await EnsurePlayableRunDocumentsAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
await EnsureGameSessionAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
|
|
|
|||
3
Aetheria.State.Daemon/Properties/AssemblyInfo.cs
Normal file
3
Aetheria.State.Daemon/Properties/AssemblyInfo.cs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Aetheria.State.Daemon.Smoke")]
|
||||
|
|
@ -2,6 +2,7 @@ using Aetheria.State.Documents;
|
|||
using GameCult.Caching;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Eve.PluginFields;
|
||||
using GameCult.Mesh;
|
||||
using GameCult.Networking;
|
||||
|
||||
|
|
@ -60,6 +61,7 @@ public static class AetheriaDocumentRegistry
|
|||
typeof(AetheriaRuntimeStarbridgeScenarioDocument),
|
||||
typeof(AetheriaRuntimeStarbridgeSessionDocument),
|
||||
typeof(AetheriaRuntimeStarbridgeSessionSummaryDocument),
|
||||
typeof(EveFieldsSplatsDocument),
|
||||
typeof(EveInputCapabilityDocument),
|
||||
typeof(AetheriaRuntimeDaemonSoaViewDocument),
|
||||
typeof(AetheriaRuntimeVerseAuthorityPolicyDocument),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
public static class AetheriaRuntimeAssets
|
||||
{
|
||||
public const string PresentationHullItemTagPrefix = "presentation-hull-item:";
|
||||
|
||||
public static string ResolveEntityPrefabAssetRef(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog = null)
|
||||
|
|
@ -15,7 +17,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
if (entity == null) return "";
|
||||
var hull = catalog?.FindItem(entity.HullItemKey ?? "");
|
||||
if (hull != null && !string.IsNullOrWhiteSpace(hull.HullPrefab))
|
||||
return HullPrefabAssetKey(hull.ItemKey);
|
||||
return HullPrefabAssetKey(ResolvePresentationHull(hull, catalog!).ItemKey);
|
||||
var kind = (entity.Kind ?? "").Trim().ToLowerInvariant();
|
||||
if (kind.Contains("station")) return "prefab.entity.station";
|
||||
if (kind.Contains("projectile")) return "prefab.entity.projectile";
|
||||
|
|
@ -79,6 +81,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
if (!string.IsNullOrWhiteSpace(item.HullPrefab))
|
||||
{
|
||||
var presentationHull = ResolvePresentationHull(item, catalog!);
|
||||
if (!string.Equals(presentationHull.ItemKey, item.ItemKey, StringComparison.Ordinal))
|
||||
continue;
|
||||
Add(entries, MapPrefab(
|
||||
HullPrefabAssetKey(item.ItemKey),
|
||||
string.IsNullOrWhiteSpace(item.Name) ? item.ItemKey : item.Name,
|
||||
|
|
@ -110,6 +115,28 @@ namespace GameCult.Aetheria.State.Verse
|
|||
};
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeCatalogItem ResolvePresentationHull(
|
||||
AetheriaRuntimeCatalogItem hull,
|
||||
AetheriaRuntimeCatalogSnapshot catalog)
|
||||
{
|
||||
var aliases = (hull.Tags ?? Array.Empty<string>())
|
||||
.Where(tag => tag != null && tag.StartsWith(PresentationHullItemTagPrefix, StringComparison.Ordinal))
|
||||
.Select(tag => tag.Substring(PresentationHullItemTagPrefix.Length).Trim())
|
||||
.Where(itemKey => itemKey.Length > 0)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (aliases.Length == 0)
|
||||
return hull;
|
||||
if (aliases.Length != 1)
|
||||
throw new InvalidOperationException($"Hull '{hull.ItemKey}' advertises multiple presentation hulls.");
|
||||
|
||||
var presentationHull = catalog.FindItem(aliases[0]);
|
||||
if (presentationHull == null || string.IsNullOrWhiteSpace(presentationHull.HullPrefab))
|
||||
throw new InvalidOperationException(
|
||||
$"Hull '{hull.ItemKey}' presentation hull '{aliases[0]}' is missing or has no provider prefab.");
|
||||
return presentationHull;
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetRef ResolveEntityIcon(AetheriaRuntimeViewportObject? obj)
|
||||
{
|
||||
if (obj?.Controlled == true)
|
||||
|
|
|
|||
|
|
@ -1766,6 +1766,27 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("AETHERIA_TRACE_EVE_SNAPSHOTS"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
var candidates = entities
|
||||
.Where(candidate => candidate != null && candidate.EntityIndex != actorIndex)
|
||||
.Select(candidate =>
|
||||
{
|
||||
var dx = candidate.PositionX - actor.PositionX;
|
||||
var dz = candidate.PositionZ - actor.PositionZ;
|
||||
var bays = candidate.DockingBays ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
|
||||
var assignments = candidate.DockingBayAssignments ?? Array.Empty<int>();
|
||||
return $"{candidate.EntityIndex}:distance={Math.Sqrt((dx * dx) + (dz * dz)):0.###}," +
|
||||
$"bays={bays.Count},assignments=[{string.Join(",", assignments)}]";
|
||||
});
|
||||
Console.WriteLine(
|
||||
$"Dock-nearest rejected actor={actorEntityKey} maxDistance={maxDistance:0.###} " +
|
||||
$"candidates=[{string.Join(";", candidates)}]");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -1864,6 +1885,10 @@ namespace GameCult.Aetheria.State.Verse
|
|||
.ToArray();
|
||||
assignments[bayIndex] = actorIndex;
|
||||
target.DockingBayAssignments = assignments.ToArray();
|
||||
actor.VelocityX = 0;
|
||||
actor.VelocityY = 0;
|
||||
actor.HelmStrafe = 0;
|
||||
actor.HelmForward = 0;
|
||||
DisarmWeapons(actor);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -663,7 +663,10 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
|
|||
records. The Eve command lane is transport, not renderer-owned state.
|
||||
- `Aetheria.State.Daemon` hosts the CultMesh Verse member and owns long-running
|
||||
Eve command acceptance for provider-owned surfaces. `--once` runs one daemon
|
||||
tick for smoke/operator use without keeping the process alive.
|
||||
tick for smoke/operator use without keeping the process alive. Its public
|
||||
CultCache registry includes the typed Eve Fields splat document that the
|
||||
daemon persists, so its own state remains reopenable after a viewport has
|
||||
been published.
|
||||
- `AetheriaEveCommandAcceptanceStatus` and the `aetheria.operations` Eve surface
|
||||
publish Eve request acceptance health, observed depth, applied counts,
|
||||
failures, and timestamps as typed state. Console logs are notification-only.
|
||||
|
|
@ -1490,6 +1493,29 @@ edges with one worker and exactly two start/transfer/completion event triplets.
|
|||
GUIDs plus missing `Thruster` equipment required to fill their authored
|
||||
mounts, so the supplemental catalog cannot overwrite live rows or silently
|
||||
revive unrelated obsolete content.
|
||||
- Dockyard content owner: `AetheriaDaemonNativeCatalog` materializes the
|
||||
daemon-authored `Zenith Dockyard` hull and its matching `Dockyard Berth` as
|
||||
typed catalog documents. The canonical Zenith and medium bay contribute
|
||||
manufacturer, presentation, thermal, and ship-size facts; the daemon-owned
|
||||
hull definition contributes the distinct 24x24 layout and twelve explicit
|
||||
`DockingBay` hardpoints. Bay count is derived from installed equipment after
|
||||
ordinary loadout generation. Until it receives a distinct baked mesh, the
|
||||
Dockyard explicitly aliases the canonical Zenith provider asset; the asset
|
||||
manifest publishes that existing asset identity instead of inventing an
|
||||
unbaked per-item prefab. Entity fixtures, docking commands, imported
|
||||
fossil rows, Eve surfaces, and Unity lowerers cannot append bays or revise
|
||||
station capacity. Stations without dedicated docking mounts retain one
|
||||
historical free-space bay path, which excludes equipment authored for a
|
||||
dedicated `DockingBay` mount. The Anchor Station selects the
|
||||
dockyard hull through the same generator entry point as every other entity;
|
||||
the released-client proof gives its pilot daemon-authored initial velocity
|
||||
that keeps it inside the docking envelope while the client negotiates, and a
|
||||
successful dock clears velocity and helm state before Ymir excludes the
|
||||
attached child body. The proof scenario also makes its one-point-hull raider
|
||||
the sole hostile faction member, so generic focus selection and the asserted
|
||||
destruction target cannot drift apart as other Ymir bodies move;
|
||||
smoke coverage proves all twelve mounts fill with matching typed equipment
|
||||
and that the old free-space path cannot consume the dedicated berth item.
|
||||
- Generation owner: one daemon loadout generator owns one advancing mutable
|
||||
CultMath random stream. A readonly struct field is forbidden because it
|
||||
mutates a defensive copy and restarts every weighted draw. Price, allegiance,
|
||||
|
|
|
|||
Loading…
Reference in a new issue