diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 37c33bf43..202cf096b 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -44,6 +44,11 @@ else if (args.Contains("--tutorial-world", StringComparer.Ordinal)) checks.RunTutorialWorld(); Console.WriteLine("Daemon tutorial world-plan smoke passed."); } +else if (args.Contains("--tutorial-materialization", StringComparer.Ordinal)) +{ + checks.RunTutorialMaterialization(); + Console.WriteLine("Daemon tutorial entity-materialization smoke passed."); +} else { checks.Run(); @@ -80,6 +85,118 @@ internal sealed class AetheriaDaemonYmirSmokeChecks public void RunTutorialTopology() => RunCheck(TutorialTopologyIsDeterministicConnectedAndRoleOwned); public void RunZoneBodies() => RunCheck(GeneratedZoneBodiesPreserveFossilHierarchy); public void RunTutorialWorld() => RunCheck(TutorialWorldMaterializesEveryTopologyZone); + public void RunTutorialMaterialization() => RunCheck(TutorialPopulationPreservesFossilEntityMechanics); + + private static void TutorialPopulationPreservesFossilEntityMechanics() + { + var catalog = TutorialPopulationCatalog(out var factions); + var settings = AetheriaDaemonTutorialTopologySettings.Fossil; + var firstWorld = AetheriaDaemonTutorialWorldGenerator.Generate(factions, 0xA37E_2026u, settings); + var secondWorld = AetheriaDaemonTutorialWorldGenerator.Generate(factions, 0xA37E_2026u, settings); + var first = AetheriaDaemonTutorialWorldMaterializer.Materialize(firstWorld, factions, catalog); + var second = AetheriaDaemonTutorialWorldMaterializer.Materialize(secondWorld, factions, catalog); + + RequireEqual(first.Topology.EntranceZoneIndex, first.PlayerZoneIndex, + "the starter ship must enter the authored tutorial entrance zone"); + var entrance = first.Zones[first.PlayerZoneIndex]; + Require(entrance.PlayerEntityIndex >= 0 && + entrance.Entities[entrance.PlayerEntityIndex].Kind == "ship" && + entrance.Entities[entrance.PlayerEntityIndex].FactionKey == factions[0].CorporationKey, + "the tutorial must materialize one protagonist starter ship as daemon world truth"); + Require(first.Zones.Values.Any(zone => zone.Entities.Any(entity => entity.Kind == "station")) && + first.Zones.Values.Any(zone => zone.Entities.Any(entity => entity.Kind == "turret")) && + first.Zones.Values.Any(zone => zone.Entities.Any(entity => + entity.Kind == "ship" && entity.AgentTaskCapabilities.Contains("attack", StringComparer.Ordinal))), + "fossil station, defensive turret, and hostile-agent population must all survive materialization"); + Require(first.Zones.Values.All(zone => + { + var orbitKeys = zone.Orbits.Select(orbit => orbit.OrbitKey).ToHashSet(StringComparer.Ordinal); + return zone.Entities.Where(entity => entity.Kind is "station" or "turret") + .All(entity => !string.IsNullOrWhiteSpace(entity.OrbitKey) && orbitKeys.Contains(entity.OrbitKey)); + }), "every orbital entity must reference an orbit owned by its canonical zone document"); + Require(first.Zones.Values.SelectMany(zone => zone.Entities) + .Where(entity => entity.Kind is "station" or "turret" or "ship") + .All(entity => entity.LoadoutGeneration != null && + entity.LoadoutGeneration.SourceZoneIndex >= 0 && + entity.LoadoutGeneration.Selections.Any(selection => selection.Role == "hull")), + "every generated entity must retain its daemon loadout provenance receipt"); + Require(first.Zones.OrderBy(pair => pair.Key) + .Select(pair => string.Join('|', pair.Value.Entities.Select(entity => + $"{entity.Kind}:{entity.FactionKey}:{entity.HullItemKey}:{entity.OrbitKey}"))) + .SequenceEqual(second.Zones.OrderBy(pair => pair.Key) + .Select(pair => string.Join('|', pair.Value.Entities.Select(entity => + $"{entity.Kind}:{entity.FactionKey}:{entity.HullItemKey}:{entity.OrbitKey}")))), + "fixed topology, catalog, and post-body random state must reproduce entity population exactly"); + } + + private static AetheriaRuntimeCatalogSnapshot TutorialPopulationCatalog( + out AetheriaDaemonTutorialFactionInput[] factions) + { + var one = new[] { new AetheriaRuntimeShapeCell(0, 0) }; + var hullCells = Enumerable.Range(0, 6).SelectMany(y => Enumerable.Range(0, 6) + .Select(x => new AetheriaRuntimeShapeCell(x, y))).ToArray(); + var manufacturer = "faction.miss"; + + AetheriaRuntimeCatalogItem Item(string key, string category, string hardpointType = "", params string[] behaviors) + { + var item = CatalogItem(key, behaviors.Select((kind, index) => + new AetheriaRuntimeBehaviorPayload(index, kind, 0, Array.Empty())).ToArray()); + item.Category = category; + item.ManufacturerKey = manufacturer; + item.Price = 10; + item.HardpointType = hardpointType; + item.ShapeWidth = 1; + item.ShapeHeight = 1; + item.OccupiedCells = 1; + item.ShapeCells = one; + item.BehaviorKinds = behaviors; + return item; + } + + AetheriaRuntimeCatalogItem Hull(string key, string hullType, string controller) + { + var hull = Item(key, AetheriaRuntimeItemCategories.Hull, "Hull"); + hull.HullType = hullType; + hull.ShapeWidth = 6; + hull.ShapeHeight = 6; + hull.OccupiedCells = hullCells.Length; + hull.ShapeCells = hullCells; + hull.Hardpoints = + [ + new AetheriaRuntimeHardpoint("ControlModule", 0, 0, 1, 1, 1, one, "", "None", 0), + new AetheriaRuntimeHardpoint("Weapon", 1, 0, 1, 1, 1, one, "", "None", 0) + ]; + return hull; + } + + var shipHull = Hull("tutorial-ship-hull", "Ship", "Cockpit"); + var stationHull = Hull("tutorial-station-hull", "Station", "TurretController"); + var cockpit = Item("tutorial-cockpit", AetheriaRuntimeItemCategories.Gear, "ControlModule", "Cockpit"); + var turret = Item("tutorial-turret-controller", AetheriaRuntimeItemCategories.Gear, "ControlModule", "TurretController"); + var weapon = Item("tutorial-weapon", AetheriaRuntimeItemCategories.Weapon, "Weapon", "LockWeapon"); + var cargo = Item("tutorial-cargo", AetheriaRuntimeItemCategories.CargoBay, "Internal"); + cargo.InteriorShapeWidth = 4; + cargo.InteriorShapeHeight = 4; + cargo.InteriorOccupiedCells = 16; + cargo.InteriorShapeCells = Enumerable.Range(0, 4).SelectMany(y => Enumerable.Range(0, 4) + .Select(x => new AetheriaRuntimeShapeCell(x, y))).ToArray(); + var docking = Item("tutorial-docking", AetheriaRuntimeItemCategories.DockingBay, "Internal"); + var reactor = Item("tutorial-reactor", AetheriaRuntimeItemCategories.Gear, "Internal", "Reactor"); + var capacitor = Item("tutorial-capacitor", AetheriaRuntimeItemCategories.Gear, "Internal", "Capacitor"); + var names = new[] { "Miss", "Zhe", "Luc", "Aero", "Finch", "Adras" }; + var corporations = names.Select(name => new AetheriaRuntimeCorporation( + $"faction.{name.ToLowerInvariant()}", name, name, "", $"names.{name}", "", 4, 1, + [new AetheriaRuntimeCorporationAllegiance(manufacturer, 1)])).ToArray(); + factions = names.Select((name, index) => new AetheriaDaemonTutorialFactionInput( + corporations[index].CorporationKey, name, name, 4) + { + TrainingNames = Enumerable.Range(0, 128).Select(value => $"{name}{value:D3}").ToArray() + }).ToArray(); + return new AetheriaRuntimeCatalogSnapshot( + [shipHull, stationHull, cockpit, turret, weapon, cargo, docking, reactor, capacitor], + corporations, + []); + } private static void TutorialWorldMaterializesEveryTopologyZone() { diff --git a/Aetheria.State.Daemon/AetheriaDaemonGeneratedEntityFactory.cs b/Aetheria.State.Daemon/AetheriaDaemonGeneratedEntityFactory.cs new file mode 100644 index 000000000..f4597059a --- /dev/null +++ b/Aetheria.State.Daemon/AetheriaDaemonGeneratedEntityFactory.cs @@ -0,0 +1,72 @@ +using Aetheria.State.Documents; + +internal static class AetheriaDaemonGeneratedEntityFactory +{ + public static AetheriaEntitySnapshot Create( + string name, + string kind, + string factionKey, + AetheriaDaemonLoadout loadout, + double x = 0, + double z = 0, + string orbitKey = "", + int securityLevel = 0, + double securityRadius = 0) + { + var orbital = IsOrbital(kind); + return new AetheriaEntitySnapshot + { + Name = name, + Kind = kind, + Position = new AetheriaVector3 { X = x, Z = z }, + Direction = new AetheriaVector2 { Y = 1 }, + LookDirection = new AetheriaVector2 { Y = 1 }, + Velocity = new AetheriaVector2(), + FactionKey = factionKey, + HullItemKey = loadout.HullItemKey, + LoadoutGeneration = loadout.Receipt, + OrbitKey = orbitKey, + SecurityLevel = securityLevel, + SecurityRadius = securityRadius, + IsActive = true, + HeatsinksEnabled = true, + Visibility = orbital ? 760 : 420, + VisibilitySourceCount = 1, + StatGrids = + [ + StatGrid("hull", orbital ? 420 : 130), + StatGrid("shield", orbital ? 130 : 50), + StatGrid("heat", 0) + ], + Equipment = loadout.Equipment, + CargoBays = loadout.CargoBays, + DockingBays = loadout.DockingBays, + WeaponGroups = loadout.WeaponGroups + .Select(indices => new AetheriaWeaponGroupSnapshot { EquipmentIndices = indices }) + .ToArray(), + CargoContents = loadout.CargoBays + .Select((_, index) => new AetheriaCargoBayLoadout + { + Items = index == 0 ? loadout.Cargo : [] + }) + .ToArray(), + DockingBayContents = loadout.DockingBays + .Select(_ => new AetheriaCargoBayLoadout()) + .ToArray(), + DockingBayAssignments = loadout.DockingBays.Select(_ => -1).ToArray() + }; + } + + private static bool IsOrbital(string kind) => + string.Equals(kind, "station", StringComparison.OrdinalIgnoreCase) || + string.Equals(kind, "turret", StringComparison.OrdinalIgnoreCase) || + string.Equals(kind, "orbital", StringComparison.OrdinalIgnoreCase); + + private static AetheriaEntityStatGrid StatGrid(string name, double value) => new() + { + Name = name, + Width = 1, + Height = 1, + Values = [value] + }; +} diff --git a/Aetheria.State.Daemon/AetheriaDaemonTutorialWorldMaterializer.cs b/Aetheria.State.Daemon/AetheriaDaemonTutorialWorldMaterializer.cs new file mode 100644 index 000000000..fb715cd7c --- /dev/null +++ b/Aetheria.State.Daemon/AetheriaDaemonTutorialWorldMaterializer.cs @@ -0,0 +1,202 @@ +using Aetheria.State.Documents; +using GameCult.Aetheria.State.Verse; + +public sealed record AetheriaDaemonMaterializedTutorialZone( + AetheriaDaemonTutorialZoneTopology Topology, + AetheriaDaemonGeneratedZonePlan CelestialPlan, + string NearestFactionKey, + int FactionPresence, + AetheriaOrbitSnapshot[] Orbits, + AetheriaEntitySnapshot[] Entities, + int PlayerEntityIndex); + +public sealed record AetheriaDaemonMaterializedTutorialWorld( + AetheriaDaemonTutorialTopology Topology, + IReadOnlyDictionary Zones, + int PlayerZoneIndex, + int PlayerEntityIndex); + +/// +/// Daemon-owned port of the fossil ZoneGenerator population pass. Celestial +/// generation hands its exact remaining random state to this organ, so adding +/// presentation work cannot perturb station, turret, or enemy placement. +/// +public static class AetheriaDaemonTutorialWorldMaterializer +{ + public static AetheriaDaemonMaterializedTutorialWorld Materialize( + AetheriaDaemonTutorialWorldPlan world, + IReadOnlyList factions, + AetheriaRuntimeCatalogSnapshot catalog) + { + if (world == null) throw new ArgumentNullException(nameof(world)); + if (catalog == null) throw new ArgumentNullException(nameof(catalog)); + var homes = world.Topology.HomeZoneByFactionKey; + var adjacency = world.Topology.Zones.ToDictionary( + zone => zone.ZoneIndex, + zone => zone.AdjacentZoneIndices, + EqualityComparer.Default); + var distances = factions.ToDictionary( + faction => faction.CorporationKey, + faction => DistancesFrom(homes[faction.CorporationKey], adjacency), + StringComparer.Ordinal); + var zones = new Dictionary(); + var playerEntityIndex = -1; + + foreach (var topology in world.Topology.Zones.OrderBy(zone => zone.ZoneIndex)) + { + var celestial = world.Zones[topology.ZoneIndex]; + var random = new CultMath.Random(celestial.PostBodyRandomState); + var nearest = factions + .OrderBy(faction => distances[faction.CorporationKey][topology.ZoneIndex]) + .First(); + var homeDistance = distances[nearest.CorporationKey][topology.ZoneIndex]; + var tutorialInfluence = Math.Max(0, (nearest.InfluenceDistance + 1) / 2); + var factionPresence = tutorialInfluence - homeDistance + 1; + var stationCount = (int)(random.NextFloat() * (factionPresence + 1)); + var orbits = celestial.Orbits.ToList(); + var entities = new List(); + var generators = new Dictionary(StringComparer.Ordinal); + + AetheriaDaemonLoadoutGenerator Generator(string factionKey) + { + if (!generators.TryGetValue(factionKey, out var generator)) + { + generator = new AetheriaDaemonLoadoutGenerator( + catalog, + (uint)random.NextInt(1, int.MaxValue), + topology.ZoneIndex, + homes, + adjacency); + generators.Add(factionKey, generator); + } + return generator; + } + + var potential = celestial.Orbits + .Where(orbit => !string.IsNullOrWhiteSpace(orbit.ParentOrbitKey)) + .Where(orbit => !celestial.Orbits.Any(sibling => + !ReferenceEquals(sibling, orbit) && + string.Equals(sibling.ParentOrbitKey, orbit.ParentOrbitKey, StringComparison.Ordinal) && + Math.Abs(sibling.Distance - orbit.Distance) < 0.1)) + .OrderBy(orbit => orbit.Distance) + .ToArray(); + var selected = potential.Skip(potential.Length / 2).Take(Math.Max(0, stationCount)).ToArray(); + for (var stationIndex = 0; stationIndex < selected.Length; stationIndex++) + { + var baseOrbit = selected[stationIndex]; + var stationOrbit = new AetheriaOrbitSnapshot + { + OrbitKey = $"tutorial.zone.{topology.ZoneIndex}.station.{stationIndex}.orbit", + ParentOrbitKey = baseOrbit.ParentOrbitKey, + Distance = baseOrbit.Distance, + Phase = baseOrbit.Phase + MathF.PI / 3 * Sign(random.NextFloat() - 0.5f), + FixedPosition = new AetheriaVector2() + }; + orbits.Add(stationOrbit); + var security = (int)((1f - MathF.Pow(random.NextFloat(), factionPresence / 2f)) * 3f); + entities.Add(AetheriaDaemonGeneratedEntityFactory.Create( + $"{topology.Name} Station {stationIndex + 1}", + "station", + nearest.CorporationKey, + Generator(nearest.CorporationKey).Build("station", nearest.CorporationKey), + orbitKey: stationOrbit.OrbitKey, + securityLevel: security, + securityRadius: celestial.Radius)); + + for (var turretIndex = 0; turretIndex < 2; turretIndex++) + { + var distanceMultiplier = turretIndex / 2; + if (turretIndex % 2 == 0) distanceMultiplier = -distanceMultiplier; + var turretOrbit = new AetheriaOrbitSnapshot + { + OrbitKey = $"tutorial.zone.{topology.ZoneIndex}.station.{stationIndex}.turret.{turretIndex}.orbit", + ParentOrbitKey = stationOrbit.ParentOrbitKey, + Distance = stationOrbit.Distance, + Phase = stationOrbit.Phase + 20f * distanceMultiplier / stationOrbit.Distance, + FixedPosition = new AetheriaVector2() + }; + orbits.Add(turretOrbit); + entities.Add(AetheriaDaemonGeneratedEntityFactory.Create( + $"{topology.Name} Turret {stationIndex + 1}-{turretIndex + 1}", + "turret", + nearest.CorporationKey, + Generator(nearest.CorporationKey).Build("turret", nearest.CorporationKey), + orbitKey: turretOrbit.OrbitKey, + securityLevel: security, + securityRadius: celestial.Radius)); + } + } + + var enemyCount = (int)(random.NextFloat() * factionPresence * 2) + stationCount; + for (var enemyIndex = 0; enemyIndex < Math.Max(0, enemyCount); enemyIndex++) + { + var enemy = AetheriaDaemonGeneratedEntityFactory.Create( + $"{nearest.ShortName} Ship {enemyIndex + 1}", + "ship", + nearest.CorporationKey, + Generator(nearest.CorporationKey).Build("ship", nearest.CorporationKey)); + enemy.AgentTaskCapabilities = ["attack", "defend", "explore"]; + entities.Add(enemy); + } + + var localPlayerIndex = -1; + if (topology.ZoneIndex == world.Topology.EntranceZoneIndex) + { + var protagonist = factions[0]; + var starter = new AetheriaDaemonLoadoutGenerator( + catalog, + world.Topology.GenerationSeed ^ 0x50A7_EA11u, + topology.ZoneIndex, + homes, + adjacency).Build("ship", protagonist.CorporationKey); + localPlayerIndex = entities.Count; + entities.Add(AetheriaDaemonGeneratedEntityFactory.Create( + "Pilot", + "ship", + protagonist.CorporationKey, + starter)); + playerEntityIndex = localPlayerIndex; + } + + zones.Add(topology.ZoneIndex, new AetheriaDaemonMaterializedTutorialZone( + topology, + celestial, + nearest.CorporationKey, + factionPresence, + orbits.ToArray(), + entities.ToArray(), + localPlayerIndex)); + } + + if (playerEntityIndex < 0) + throw new InvalidOperationException("Tutorial materialization produced no player entry ship."); + return new AetheriaDaemonMaterializedTutorialWorld( + world.Topology, + zones, + world.Topology.EntranceZoneIndex, + playerEntityIndex); + } + + private static IReadOnlyDictionary DistancesFrom( + int start, + IReadOnlyDictionary> adjacency) + { + var result = adjacency.Keys.ToDictionary(index => index, _ => int.MaxValue); + var queue = new Queue(); + result[start] = 0; + queue.Enqueue(start); + while (queue.Count > 0) + { + var current = queue.Dequeue(); + foreach (var neighbor in adjacency[current]) + { + if (result[neighbor] <= result[current] + 1) continue; + result[neighbor] = result[current] + 1; + queue.Enqueue(neighbor); + } + } + return result; + } + + private static int Sign(float value) => value < 0 ? -1 : value > 0 ? 1 : 0; +} diff --git a/Aetheria.State.Daemon/AetheriaDaemonZoneBodyGenerator.cs b/Aetheria.State.Daemon/AetheriaDaemonZoneBodyGenerator.cs index 16a51efd2..a7a125d1c 100644 --- a/Aetheria.State.Daemon/AetheriaDaemonZoneBodyGenerator.cs +++ b/Aetheria.State.Daemon/AetheriaDaemonZoneBodyGenerator.cs @@ -2,6 +2,7 @@ using Aetheria.State.Documents; public sealed record AetheriaDaemonGeneratedZonePlan( uint GenerationSeed, + uint PostBodyRandomState, double Radius, double Mass, double HierarchyMass, @@ -121,6 +122,7 @@ public static class AetheriaDaemonZoneBodyGenerator return new AetheriaDaemonGeneratedZonePlan( zoneSeed, + random.state, radius, mass, planets.Sum(planet => (double)planet.Mass), diff --git a/Aetheria.State.Daemon/AetheriaDaemonZoneGenerator.cs b/Aetheria.State.Daemon/AetheriaDaemonZoneGenerator.cs index 2d967faff..46a4336ba 100644 --- a/Aetheria.State.Daemon/AetheriaDaemonZoneGenerator.cs +++ b/Aetheria.State.Daemon/AetheriaDaemonZoneGenerator.cs @@ -389,54 +389,27 @@ internal static class AetheriaDaemonZoneGenerator { var availabilityFaction = availabilityFactions[faction]; var loadout = loadouts[availabilityFaction].Build(kind, availabilityFaction); - return new AetheriaEntitySnapshot + var entity = AetheriaDaemonGeneratedEntityFactory.Create( + name, + kind, + faction, + loadout, + x, + z); + entity.Direction = Vec2(vx == 0 && vy == 0 ? 0 : vx, vx == 0 && vy == 0 ? 1 : vy); + entity.LookDirection = Vec2(vx == 0 && vy == 0 ? 0 : vx, vx == 0 && vy == 0 ? 1 : vy); + entity.Velocity = Vec2(vx, vy); + entity.Visibility = visibility; + entity.TargetEntityKey = target; + entity.Contacts = contactKeys.Select(key => new AetheriaEntityContactSnapshot { - Name = name, - Kind = kind, - Position = Vec3(x, 0, z), - Direction = Vec2(vx == 0 && vy == 0 ? 0 : vx, vx == 0 && vy == 0 ? 1 : vy), - LookDirection = Vec2(vx == 0 && vy == 0 ? 0 : vx, vx == 0 && vy == 0 ? 1 : vy), - Velocity = Vec2(vx, vy), - FactionKey = faction, - HullItemKey = loadout.HullItemKey, - LoadoutGeneration = loadout.Receipt, - IsActive = true, - HeatsinksEnabled = true, - TractorPower = 0, - Visibility = visibility, - VisibilitySourceCount = 1, - TargetEntityKey = target, - Contacts = contactKeys.Select(key => new AetheriaEntityContactSnapshot - { - TargetEntityKey = key, - InfoGathered = 1, - Hostile = string.Equals(faction, "player", StringComparison.OrdinalIgnoreCase) && key.Contains(".entity.6.", StringComparison.Ordinal) - }).ToArray(), - StatGrids = - [ - StatGrid("hull", string.Equals(kind, "station", StringComparison.OrdinalIgnoreCase) ? 420 : string.Equals(faction, "raider", StringComparison.OrdinalIgnoreCase) ? 85 : 130), - StatGrid("shield", string.Equals(kind, "station", StringComparison.OrdinalIgnoreCase) ? 130 : 50), - StatGrid("heat", 0) - ], - Equipment = loadout.Equipment, - CargoBays = loadout.CargoBays, - DockingBays = loadout.DockingBays, - WeaponGroups = loadout.WeaponGroups - .Select(indices => new AetheriaWeaponGroupSnapshot { EquipmentIndices = indices }) - .ToArray(), - CargoContents = loadout.CargoBays - .Select((_, index) => new AetheriaCargoBayLoadout - { - Items = index == 0 ? loadout.Cargo : [] - }) - .ToArray(), - DockingBayContents = loadout.DockingBays - .Select(_ => new AetheriaCargoBayLoadout()) - .ToArray(), - DockingBayAssignments = loadout.DockingBays - .Select(_ => -1) - .ToArray() - }; + TargetEntityKey = key, + InfoGathered = 1, + Hostile = string.Equals(faction, "player", StringComparison.OrdinalIgnoreCase) && key.Contains(".entity.6.", StringComparison.Ordinal) + }).ToArray(); + if (string.Equals(faction, "raider", StringComparison.OrdinalIgnoreCase)) + entity.StatGrids[0].Values[0] = 85; + return entity; } private static AetheriaGasGiantVisualState GasGiantVisual(BodyPlan body) diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index bef71ebe4..3e7357436 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -2691,6 +2691,9 @@ static AetheriaRuntimeEntitySnapshotCommit ToRuntimeEntity( LookDirectionY = entity.LookDirection?.Y ?? entity.Direction?.Y ?? 1, HelmStrafe = entity.HelmInput?.X ?? 0, HelmForward = entity.HelmInput?.Y ?? 0, + OrbitKey = entity.OrbitKey ?? "", + SecurityLevel = entity.SecurityLevel, + SecurityRadius = entity.SecurityRadius, FactionKey = entity.FactionKey ?? "", HullItemKey = entity.HullItemKey ?? "", Equipment = equipment, diff --git a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs index c408fcc65..59d71d0fa 100644 --- a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs +++ b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs @@ -633,6 +633,15 @@ public sealed class AetheriaEntitySnapshot [Key(34)] public AetheriaVector2 HelmInput { get; set; } = new(); + + [Key(35)] + public string OrbitKey { get; set; } = ""; + + [Key(36)] + public int SecurityLevel { get; set; } + + [Key(37)] + public double SecurityRadius { get; set; } } [MessagePackObject] diff --git a/Assets/Scripts/Gameplay/AetheriaUnityEntityBlueprintMaterializer.cs b/Assets/Scripts/Gameplay/AetheriaUnityEntityBlueprintMaterializer.cs index 6b1cf904d..46b417a94 100644 --- a/Assets/Scripts/Gameplay/AetheriaUnityEntityBlueprintMaterializer.cs +++ b/Assets/Scripts/Gameplay/AetheriaUnityEntityBlueprintMaterializer.cs @@ -85,6 +85,12 @@ public sealed class AetheriaUnityEntityBlueprintMaterializer shipBlueprint.Direction = new float2((float)entity.DirectionX, (float)entity.DirectionY); shipBlueprint.IsPlayerShip = isCurrentEntity; } + else if (blueprint is OrbitalEntityConstructionBlueprint orbitalBlueprint) + { + orbitalBlueprint.OrbitKey = entity.OrbitKey ?? ""; + orbitalBlueprint.SecurityLevel = (SecurityLevel)entity.SecurityLevel; + orbitalBlueprint.SecurityRadius = (float)entity.SecurityRadius; + } blueprint.Name = entity.Name ?? ""; blueprint.FactionKey = entity.FactionKey ?? ""; @@ -102,7 +108,9 @@ public sealed class AetheriaUnityEntityBlueprintMaterializer private static EntityConstructionBlueprint CreateBlueprint(string kind) { - return string.Equals(kind, "orbital", StringComparison.OrdinalIgnoreCase) + return string.Equals(kind, "orbital", StringComparison.OrdinalIgnoreCase) || + string.Equals(kind, "station", StringComparison.OrdinalIgnoreCase) || + string.Equals(kind, "turret", StringComparison.OrdinalIgnoreCase) ? new OrbitalEntityConstructionBlueprint() : new ShipConstructionBlueprint(); } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs index 4d0496043..d69f9733c 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs @@ -921,7 +921,10 @@ namespace GameCult.Aetheria.State.Verse double lookDirectionX = 0, double lookDirectionY = 0, double helmStrafe = 0, - double helmForward = 0) + double helmForward = 0, + string orbitKey = "", + int securityLevel = 0, + double securityRadius = 0) { RecordKey = recordKey; EntityIndex = entityIndex; @@ -964,6 +967,9 @@ namespace GameCult.Aetheria.State.Verse LookDirectionY = lookDirectionY; HelmStrafe = helmStrafe; HelmForward = helmForward; + OrbitKey = orbitKey ?? ""; + SecurityLevel = securityLevel; + SecurityRadius = securityRadius; } public string RecordKey { get; set; } @@ -1007,6 +1013,9 @@ namespace GameCult.Aetheria.State.Verse public double LookDirectionY { get; set; } public double HelmStrafe { get; set; } public double HelmForward { get; set; } + public string OrbitKey { get; set; } + public int SecurityLevel { get; set; } + public double SecurityRadius { get; set; } } [MessagePackObject(true)] diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs index 02bf73672..d89c3e1e5 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs @@ -1032,7 +1032,10 @@ namespace GameCult.Aetheria.State.Verse entity.LookDirectionX, entity.LookDirectionY, entity.HelmStrafe, - entity.HelmForward); + entity.HelmForward, + entity.OrbitKey, + entity.SecurityLevel, + entity.SecurityRadius); } private static AetheriaRuntimeEntityItemSlotSnapshot[] CreateEntityItemSlots( diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs index a6d2e2171..973ee26a6 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs @@ -1137,6 +1137,12 @@ namespace GameCult.Aetheria.State.Verse [Key(54)] public IReadOnlyList ActiveWeaponGroups { get; set; } = Array.Empty(); + + [Key(55)] + public int SecurityLevel { get; set; } + + [Key(56)] + public double SecurityRadius { get; set; } } [MessagePackObject]