Refine ship orders and viewer controls
This commit is contained in:
@@ -314,24 +314,28 @@ public sealed class SpatialBuilder
|
||||
ResourceNodeDefinition definition,
|
||||
float oreAmount)
|
||||
{
|
||||
var depositCount = Math.Clamp((int)MathF.Round(MathF.Sqrt(MathF.Max(oreAmount, 1f)) / 18f), 4, 12);
|
||||
var derivedDepositCount = Math.Clamp((int)MathF.Round(MathF.Sqrt(MathF.Max(oreAmount, 1f)) / 18f), 4, 18);
|
||||
var depositCount = Math.Clamp(definition.ShardCount > 0 ? definition.ShardCount : derivedDepositCount, 4, 48);
|
||||
var deposits = new List<ResourceDepositRuntime>(depositCount);
|
||||
var weightTotal = 0f;
|
||||
var weights = new float[depositCount];
|
||||
var random = new Random(ComputeDeterministicSeed(systemId, nodeId, "resource-deposits"));
|
||||
for (var index = 0; index < depositCount; index += 1)
|
||||
{
|
||||
var weight = 0.8f + (Hash01(systemId, nodeId, $"weight-{index}") * 1.6f);
|
||||
var weight = 0.8f + (NextFloat01(random) * 1.6f);
|
||||
weights[index] = weight;
|
||||
weightTotal += weight;
|
||||
}
|
||||
|
||||
var scatterRadius = MathF.Max(140f, LocalSpaceRadius * 0.58f);
|
||||
// Resource node localspace should read as a compact mineable field around the node core,
|
||||
// not as sparse debris spread across the entire anchor volume.
|
||||
var scatterRadius = MathF.Max(120f, MathF.Min(LocalSpaceRadius * 0.2f, 900f));
|
||||
for (var index = 0; index < depositCount; index += 1)
|
||||
{
|
||||
var angle = Hash01(systemId, nodeId, $"angle-{index}") * MathF.PI * 2f;
|
||||
var radiusFactor = 0.22f + (Hash01(systemId, nodeId, $"radius-{index}") * 0.74f);
|
||||
var angle = NextFloat01(random) * MathF.PI * 2f;
|
||||
var radiusFactor = 0.12f + (NextFloat01(random) * 0.82f);
|
||||
var radius = scatterRadius * MathF.Sqrt(radiusFactor);
|
||||
var vertical = (Hash01(systemId, nodeId, $"vertical-{index}") - 0.5f) * MathF.Max(60f, scatterRadius * 0.14f);
|
||||
var vertical = (NextFloat01(random) - 0.5f) * MathF.Max(40f, scatterRadius * 0.18f);
|
||||
var localPosition = new Vector3(
|
||||
MathF.Cos(angle) * radius,
|
||||
vertical,
|
||||
@@ -351,6 +355,32 @@ public sealed class SpatialBuilder
|
||||
return deposits;
|
||||
}
|
||||
|
||||
private static int ComputeDeterministicSeed(string systemId, string nodeId, string salt)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hash = 17;
|
||||
foreach (var character in systemId)
|
||||
{
|
||||
hash = (hash * 31) + character;
|
||||
}
|
||||
|
||||
foreach (var character in nodeId)
|
||||
{
|
||||
hash = (hash * 31) + character;
|
||||
}
|
||||
|
||||
foreach (var character in salt)
|
||||
{
|
||||
hash = (hash * 31) + character;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
private static float NextFloat01(Random random) => (float)random.NextDouble();
|
||||
|
||||
private static float Hash01(string systemId, string nodeId, string salt)
|
||||
{
|
||||
unchecked
|
||||
@@ -391,13 +421,15 @@ public sealed class SpatialBuilder
|
||||
|
||||
internal static ShipSpatialStateRuntime CreateInitialShipSpatialState(string systemId, Vector3 position, IReadOnlyCollection<AnchorRuntime> anchors)
|
||||
{
|
||||
var systemPosition = SimulationUnits.MetersToKilometers(position);
|
||||
var nearestAnchor = anchors
|
||||
.Where(anchor => anchor.SystemId == systemId)
|
||||
.OrderBy(anchor => anchor.Position.DistanceTo(position))
|
||||
.OrderBy(anchor => anchor.Position.DistanceTo(systemPosition))
|
||||
.FirstOrDefault();
|
||||
var localPosition = nearestAnchor is null
|
||||
? position
|
||||
: position.Subtract(nearestAnchor.Position);
|
||||
var localPosition = position;
|
||||
var resolvedSystemPosition = nearestAnchor is null
|
||||
? systemPosition
|
||||
: Add(nearestAnchor.Position, SimulationUnits.MetersToKilometers(localPosition));
|
||||
|
||||
return new ShipSpatialStateRuntime
|
||||
{
|
||||
@@ -405,7 +437,7 @@ public sealed class SpatialBuilder
|
||||
SpaceLayer = SpaceLayerKind.LocalSpace,
|
||||
CurrentAnchorId = nearestAnchor?.Id,
|
||||
LocalPosition = localPosition,
|
||||
SystemPosition = position,
|
||||
SystemPosition = resolvedSystemPosition,
|
||||
MovementRegime = MovementRegimeKind.LocalFlight,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,11 @@ public sealed class WorldBuilder(
|
||||
WorldGenerationOptions worldGenerationOptions,
|
||||
ScenarioDefinition? scenarioDefinition)
|
||||
{
|
||||
var topology = topologyBuilder.Build(worldGenerationOptions);
|
||||
// Temporary QA override: allow a scenario to provide an exact system list
|
||||
// instead of going through procedural topology generation.
|
||||
var topology = scenarioDefinition?.Systems is { Count: > 0 } scenarioSystems
|
||||
? topologyBuilder.Build(scenarioSystems)
|
||||
: topologyBuilder.Build(worldGenerationOptions);
|
||||
var scenario = scenarioDefinition ?? scenarioValidationService.CreateEmptyScenario(worldGenerationOptions, topology.Systems);
|
||||
scenarioValidationService.Validate(scenario, topology.Systems.Select(system => system.Id).ToHashSet(StringComparer.Ordinal));
|
||||
|
||||
|
||||
@@ -13,6 +13,22 @@ public sealed class WorldTopologyBuilder(
|
||||
generationService.PrepareKnownSystems(staticData.KnownSystems),
|
||||
worldGenerationOptions);
|
||||
|
||||
return BuildFromDefinitions(systems);
|
||||
}
|
||||
|
||||
public WorldBuildTopology Build(IReadOnlyList<SolarSystemDefinition> systems)
|
||||
{
|
||||
if (systems.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Scenario-defined systems cannot be empty.");
|
||||
}
|
||||
|
||||
// Temporary QA-only path for fixed-topology scenarios such as "minimal".
|
||||
return BuildFromDefinitions(systems);
|
||||
}
|
||||
|
||||
private WorldBuildTopology BuildFromDefinitions(IReadOnlyList<SolarSystemDefinition> systems)
|
||||
{
|
||||
var systemRuntimes = systems
|
||||
.Select(definition => new SystemRuntime
|
||||
{
|
||||
|
||||
@@ -308,9 +308,10 @@ internal sealed class OrbitalStateUpdater
|
||||
}
|
||||
|
||||
ship.SpatialState.CurrentAnchorId = currentAnchor?.Id;
|
||||
var localSystemOffset = SimulationUnits.MetersToKilometers(ship.Position);
|
||||
ship.SpatialState.SystemPosition = currentAnchor is null
|
||||
? ship.Position
|
||||
: Add(currentAnchor.Position, ship.Position);
|
||||
? localSystemOffset
|
||||
: Add(currentAnchor.Position, localSystemOffset);
|
||||
|
||||
if (ship.DockedStationId is null)
|
||||
{
|
||||
|
||||
@@ -129,6 +129,39 @@ public sealed class WorldService
|
||||
}
|
||||
}
|
||||
|
||||
public ShipSnapshot? UpdateShipOrder(string shipId, string orderId, ShipOrderUpdateCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
ValidateShipOrderRequestUnsafe(shipId, ToCommandRequest(request));
|
||||
var ship = CanCurrentActorAccessGm()
|
||||
? UpdateGmShipOrderUnsafe(shipId, orderId, request)
|
||||
: _playerFaction.UpdateDirectShipOrder(_world, _playerStateStore, GetCurrentPlayerKey(), shipId, orderId, request);
|
||||
if (ship is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetShipSnapshotUnsafe(ship.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public ShipSnapshot? ReorderShipOrder(string shipId, string orderId, ShipOrderReorderRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
var ship = CanCurrentActorAccessGm()
|
||||
? ReorderGmShipOrderUnsafe(shipId, orderId, request.TargetIndex)
|
||||
: _playerFaction.ReorderDirectShipOrder(_world, _playerStateStore, GetCurrentPlayerKey(), shipId, orderId, request.TargetIndex);
|
||||
if (ship is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetShipSnapshotUnsafe(ship.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public ShipSnapshot? UpdateShipDefaultBehavior(string shipId, ShipDefaultBehaviorCommandRequest request)
|
||||
{
|
||||
lock (_sync)
|
||||
@@ -694,6 +727,30 @@ public sealed class WorldService
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyShipOrderRequest(ShipOrderRuntime order, ShipOrderUpdateCommandRequest request)
|
||||
{
|
||||
order.Priority = request.Priority;
|
||||
order.InterruptCurrentPlan = request.InterruptCurrentPlan;
|
||||
order.Label = request.Label;
|
||||
order.TargetEntityId = request.TargetEntityId;
|
||||
order.TargetSystemId = request.TargetSystemId;
|
||||
order.TargetPosition = request.TargetPosition is null
|
||||
? null
|
||||
: new Vector3(request.TargetPosition.X, request.TargetPosition.Y, request.TargetPosition.Z);
|
||||
order.SourceStationId = request.SourceStationId;
|
||||
order.DestinationStationId = request.DestinationStationId;
|
||||
order.ItemId = request.ItemId;
|
||||
order.AnchorId = request.AnchorId;
|
||||
order.ConstructionSiteId = request.ConstructionSiteId;
|
||||
order.ModuleId = request.ModuleId;
|
||||
order.WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? 0f);
|
||||
order.Radius = MathF.Max(0f, request.Radius ?? 0f);
|
||||
order.MaxSystemRange = request.MaxSystemRange;
|
||||
order.KnownStationsOnly = request.KnownStationsOnly ?? false;
|
||||
order.Status = OrderStatus.Queued;
|
||||
order.FailureReason = null;
|
||||
}
|
||||
|
||||
private ShipRuntime? EnqueueGmShipOrderUnsafe(string shipId, ShipOrderCommandRequest request)
|
||||
{
|
||||
var ship = _world.Ships.FirstOrDefault(candidate => candidate.Id == shipId);
|
||||
@@ -702,12 +759,7 @@ public sealed class WorldService
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ship.OrderQueue.Count >= 8)
|
||||
{
|
||||
throw new InvalidOperationException("Order queue is full.");
|
||||
}
|
||||
|
||||
ship.OrderQueue.Add(new ShipOrderRuntime
|
||||
ship.OrderQueue.EnqueuePlayerOrder(new ShipOrderRuntime
|
||||
{
|
||||
Id = $"order-{ship.Id}-{Guid.NewGuid():N}",
|
||||
Kind = request.Kind,
|
||||
@@ -732,12 +784,7 @@ public sealed class WorldService
|
||||
});
|
||||
|
||||
ship.ControlSourceKind = "gm-order";
|
||||
ship.ControlSourceId = ship.OrderQueue
|
||||
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
|
||||
.OrderByDescending(order => order.Priority)
|
||||
.ThenBy(order => order.CreatedAtUtc)
|
||||
.Select(order => order.Id)
|
||||
.FirstOrDefault();
|
||||
ship.ControlSourceId = ship.OrderQueue.FindLeadingOrderForSource(ShipOrderSourceKind.Player)?.Id;
|
||||
ship.ControlReason = request.Label ?? request.Kind;
|
||||
ship.NeedsReplan = true;
|
||||
ship.LastReplanReason = "gm-order-enqueued";
|
||||
@@ -753,22 +800,12 @@ public sealed class WorldService
|
||||
return null;
|
||||
}
|
||||
|
||||
ship.OrderQueue.RemoveAll(order => order.Id == orderId);
|
||||
ship.ControlSourceKind = ship.OrderQueue.Any(order => order.SourceKind == ShipOrderSourceKind.Player)
|
||||
ship.OrderQueue.RemoveById(orderId);
|
||||
ship.ControlSourceKind = ship.OrderQueue.HasOrdersFromSource(ShipOrderSourceKind.Player)
|
||||
? "gm-order"
|
||||
: "gm-manual";
|
||||
ship.ControlSourceId = ship.OrderQueue
|
||||
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
|
||||
.OrderByDescending(order => order.Priority)
|
||||
.ThenBy(order => order.CreatedAtUtc)
|
||||
.Select(order => order.Id)
|
||||
.FirstOrDefault();
|
||||
ship.ControlReason = ship.OrderQueue
|
||||
.Where(order => order.SourceKind == ShipOrderSourceKind.Player)
|
||||
.OrderByDescending(order => order.Priority)
|
||||
.ThenBy(order => order.CreatedAtUtc)
|
||||
.Select(order => order.Label ?? order.Kind)
|
||||
.FirstOrDefault()
|
||||
ship.ControlSourceId = ship.OrderQueue.FindLeadingOrderForSource(ShipOrderSourceKind.Player)?.Id;
|
||||
ship.ControlReason = ship.OrderQueue.GetLeadingOrderLabelForSource(ShipOrderSourceKind.Player)
|
||||
?? "manual-gm-control";
|
||||
ship.NeedsReplan = true;
|
||||
ship.LastReplanReason = "gm-order-removed";
|
||||
@@ -776,6 +813,59 @@ public sealed class WorldService
|
||||
return ship;
|
||||
}
|
||||
|
||||
private ShipRuntime? UpdateGmShipOrderUnsafe(string shipId, string orderId, ShipOrderUpdateCommandRequest request)
|
||||
{
|
||||
var ship = _world.Ships.FirstOrDefault(candidate => candidate.Id == shipId);
|
||||
if (ship is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var order = ship.OrderQueue.FindById(orderId);
|
||||
if (order is null || order.SourceKind != ShipOrderSourceKind.Player)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ApplyShipOrderRequest(order, request);
|
||||
ship.ControlSourceKind = ship.OrderQueue.HasOrdersFromSource(ShipOrderSourceKind.Player)
|
||||
? "gm-order"
|
||||
: "gm-manual";
|
||||
ship.ControlSourceId = ship.OrderQueue.FindLeadingOrderForSource(ShipOrderSourceKind.Player)?.Id;
|
||||
ship.ControlReason = ship.OrderQueue.GetLeadingOrderLabelForSource(ShipOrderSourceKind.Player)
|
||||
?? request.Label
|
||||
?? request.Kind;
|
||||
ship.NeedsReplan = true;
|
||||
ship.LastReplanReason = "gm-order-updated";
|
||||
ship.LastDeltaSignature = string.Empty;
|
||||
return ship;
|
||||
}
|
||||
|
||||
private ShipRuntime? ReorderGmShipOrderUnsafe(string shipId, string orderId, int targetIndex)
|
||||
{
|
||||
var ship = _world.Ships.FirstOrDefault(candidate => candidate.Id == shipId);
|
||||
if (ship is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!ship.OrderQueue.TryMovePlayerOrder(orderId, targetIndex))
|
||||
{
|
||||
return ship;
|
||||
}
|
||||
|
||||
ship.ControlSourceKind = ship.OrderQueue.HasOrdersFromSource(ShipOrderSourceKind.Player)
|
||||
? "gm-order"
|
||||
: "gm-manual";
|
||||
ship.ControlSourceId = ship.OrderQueue.FindLeadingOrderForSource(ShipOrderSourceKind.Player)?.Id;
|
||||
ship.ControlReason = ship.OrderQueue.GetLeadingOrderLabelForSource(ShipOrderSourceKind.Player)
|
||||
?? "manual-gm-control";
|
||||
ship.NeedsReplan = true;
|
||||
ship.LastReplanReason = "gm-order-reordered";
|
||||
ship.LastDeltaSignature = string.Empty;
|
||||
return ship;
|
||||
}
|
||||
|
||||
private ShipRuntime? ConfigureGmShipBehaviorUnsafe(string shipId, ShipDefaultBehaviorCommandRequest request)
|
||||
{
|
||||
var ship = _world.Ships.FirstOrDefault(candidate => candidate.Id == shipId);
|
||||
@@ -837,6 +927,26 @@ public sealed class WorldService
|
||||
return ship;
|
||||
}
|
||||
|
||||
private static ShipOrderCommandRequest ToCommandRequest(ShipOrderUpdateCommandRequest request) =>
|
||||
new(
|
||||
request.Kind,
|
||||
request.Priority,
|
||||
request.InterruptCurrentPlan,
|
||||
request.Label,
|
||||
request.TargetEntityId,
|
||||
request.TargetSystemId,
|
||||
request.TargetPosition,
|
||||
request.SourceStationId,
|
||||
request.DestinationStationId,
|
||||
request.ItemId,
|
||||
request.AnchorId,
|
||||
request.ConstructionSiteId,
|
||||
request.ModuleId,
|
||||
request.WaitSeconds,
|
||||
request.Radius,
|
||||
request.MaxSystemRange,
|
||||
request.KnownStationsOnly);
|
||||
|
||||
private CommanderRuntime CreateFactionCommander(FactionRuntime faction) => new()
|
||||
{
|
||||
Id = $"commander-faction-{faction.Id}",
|
||||
@@ -915,12 +1025,15 @@ public sealed class WorldService
|
||||
return new Vector3(MathF.Cos(angle) * radius, 0f, MathF.Sin(angle) * radius);
|
||||
}
|
||||
|
||||
private AnchorRuntime? ResolveNearestConstructibleAnchor(string systemId, Vector3 position) =>
|
||||
_world.Anchors
|
||||
.Where(candidate => string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal))
|
||||
.Where(candidate => SpatialBuilder.IsConstructibleAnchorKind(candidate.Kind))
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(position))
|
||||
.FirstOrDefault();
|
||||
private AnchorRuntime? ResolveNearestConstructibleAnchor(string systemId, Vector3 position)
|
||||
{
|
||||
var systemPosition = SimulationUnits.MetersToKilometers(position);
|
||||
return _world.Anchors
|
||||
.Where(candidate => string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal))
|
||||
.Where(candidate => SpatialBuilder.IsConstructibleAnchorKind(candidate.Kind))
|
||||
.OrderBy(candidate => candidate.Position.DistanceTo(systemPosition))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private string? ResolveNearestAnchorId(string systemId, Vector3 position) =>
|
||||
ResolveNearestConstructibleAnchor(systemId, position)?.Id;
|
||||
|
||||
Reference in New Issue
Block a user