Refine ship orders and viewer controls

This commit is contained in:
2026-04-09 12:42:52 -04:00
parent 6c92ab50c8
commit 8503855a4c
64 changed files with 2939 additions and 2037 deletions

View File

@@ -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;