using System.Text.Json; using SpaceGame.Simulation.Api.Data; namespace SpaceGame.Simulation.Api.Simulation; public sealed class ScenarioLoader { private readonly string _dataRoot; private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true, }; public ScenarioLoader(string contentRootPath) { _dataRoot = Path.GetFullPath(Path.Combine(contentRootPath, "..", "..", "shared", "data")); } public SimulationWorld Load() { var systems = Read>("systems.json"); var scenario = Read("scenario.json"); var ships = Read>("ships.json"); var constructibles = Read>("constructibles.json"); var balance = Read("balance.json"); var shipDefinitions = ships.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var constructibleDefinitions = constructibles.ToDictionary((definition) => definition.Id, StringComparer.Ordinal); var systemRuntimes = systems .Select((definition) => new SystemRuntime { Definition = definition, Position = ToVector(definition.Position), }) .ToList(); var systemsById = systemRuntimes.ToDictionary((system) => system.Definition.Id, StringComparer.Ordinal); var nodes = new List(); var nodeIdCounter = 0; foreach (var system in systemRuntimes) { foreach (var node in system.Definition.ResourceNodes) { nodes.Add(new ResourceNodeRuntime { Id = $"node-{++nodeIdCounter}", SystemId = system.Definition.Id, Position = new Vector3( system.Position.X + (MathF.Cos(node.Angle) * node.RadiusOffset), balance.YPlane, system.Position.Z + (MathF.Sin(node.Angle) * node.RadiusOffset)), ItemId = node.ItemId, OreRemaining = node.OreAmount, MaxOre = node.OreAmount, }); } } var stations = new List(); var stationIdCounter = 0; foreach (var plan in scenario.InitialStations) { if (!constructibleDefinitions.TryGetValue(plan.ConstructibleId, out var definition) || !systemsById.TryGetValue(plan.SystemId, out var system)) { continue; } stations.Add(new StationRuntime { Id = $"station-{++stationIdCounter}", SystemId = system.Definition.Id, Definition = definition, Position = ResolveStationPosition(system, plan, balance), FactionId = plan.FactionId ?? "sol-dominion", OreStored = definition.Category == "refining" ? 120f : 0f, RefinedStock = definition.Category == "shipyard" ? 180f : 40f, }); } var refinery = stations.FirstOrDefault((station) => station.Definition.Category == "refining" && station.SystemId == scenario.MiningDefaults.RefinerySystemId) ?? stations.FirstOrDefault((station) => station.Definition.Category == "refining"); var patrolRoutes = scenario.PatrolRoutes.ToDictionary( (route) => route.SystemId, (route) => route.Points.Select(ToVector).ToList(), StringComparer.Ordinal); var shipsRuntime = new List(); var shipIdCounter = 0; foreach (var formation in scenario.ShipFormations) { if (!shipDefinitions.TryGetValue(formation.ShipId, out var definition)) { continue; } for (var index = 0; index < formation.Count; index += 1) { var offset = new Vector3((index % 3) * 18f, balance.YPlane, (index / 3) * 18f); var position = Add(ToVector(formation.Center), offset); shipsRuntime.Add(new ShipRuntime { Id = $"ship-{++shipIdCounter}", SystemId = formation.SystemId, Definition = definition, FactionId = formation.FactionId ?? "sol-dominion", Position = position, TargetPosition = position, DefaultBehavior = CreateBehavior(definition, formation.SystemId, scenario, patrolRoutes, refinery), ControllerTask = new ControllerTaskRuntime { Kind = "idle", Threshold = balance.ArrivalThreshold }, Health = definition.MaxHealth, }); } } var factions = new List { new() { Id = "sol-dominion", Label = "Sol Dominion", Color = "#7ed4ff", Credits = 240f, }, }; return new SimulationWorld { Label = "Split Viewer / Simulation World", Seed = 1, Balance = balance, Systems = systemRuntimes, Nodes = nodes, Stations = stations, Ships = shipsRuntime, Factions = factions, ShipDefinitions = shipDefinitions, GeneratedAtUtc = DateTimeOffset.UtcNow, }; } private T Read(string fileName) { var path = Path.Combine(_dataRoot, fileName); var json = File.ReadAllText(path); return JsonSerializer.Deserialize(json, _jsonOptions) ?? throw new InvalidOperationException($"Unable to read {fileName}."); } private static DefaultBehaviorRuntime CreateBehavior( ShipDefinition definition, string systemId, ScenarioDefinition scenario, IReadOnlyDictionary> patrolRoutes, StationRuntime? refinery) { if (definition.Role == "mining" && refinery is not null) { return new DefaultBehaviorRuntime { Kind = "auto-mine", AreaSystemId = scenario.MiningDefaults.NodeSystemId, RefineryId = refinery.Id, Phase = "travel-to-node", }; } if (definition.Role == "military" && patrolRoutes.TryGetValue(systemId, out var route)) { return new DefaultBehaviorRuntime { Kind = "patrol", PatrolPoints = route, PatrolIndex = 0, }; } return new DefaultBehaviorRuntime { Kind = "idle", }; } private static Vector3 ResolveStationPosition(SystemRuntime system, InitialStationDefinition plan, BalanceDefinition balance) { if (plan.Position is { Length: 3 }) { return ToVector(plan.Position); } if (plan.PlanetIndex is int planetIndex && planetIndex >= 0 && planetIndex < system.Definition.Planets.Count) { var planet = system.Definition.Planets[planetIndex]; var side = plan.LagrangeSide ?? 1; return new Vector3( system.Position.X + planet.OrbitRadius + (side * 72f), balance.YPlane, system.Position.Z + ((planetIndex + 1) * 42f * side)); } return new Vector3(system.Position.X + 180f, balance.YPlane, system.Position.Z); } private static Vector3 ToVector(float[] values) => new(values[0], values[1], values[2]); private static Vector3 Add(Vector3 left, Vector3 right) => new(left.X + right.X, left.Y + right.Y, left.Z + right.Z); }