namespace SpaceGame.Api.Ships.Runtime; public sealed class ShipRuntime { public required string Id { get; init; } public required string SystemId { get; set; } public required ShipDefinition Definition { get; init; } public required string FactionId { get; init; } public required Vector3 Position { get; set; } public required Vector3 TargetPosition { get; set; } public required ShipSpatialStateRuntime SpatialState { get; set; } public Vector3 Velocity { get; set; } = Vector3.Zero; public ShipState State { get; set; } = ShipState.Idle; public required DefaultBehaviorRuntime DefaultBehavior { get; set; } public ShipOrderQueue OrderQueue { get; } = new(); public required ShipSkillProfileRuntime Skills { get; set; } public bool NeedsReplan { get; set; } = true; public float ReplanCooldownSeconds { get; set; } public Dictionary Inventory { get; } = new(StringComparer.Ordinal); public string? DockedStationId { get; set; } public int? AssignedDockingPadIndex { get; set; } public string? CommanderId { get; set; } public string? PolicySetId { get; set; } public string ControlSourceKind { get; set; } = "unassigned"; public string? ControlSourceId { get; set; } public string? ControlReason { get; set; } public string? LastReplanReason { get; set; } public string? LastAccessFailureReason { get; set; } public float Health { get; set; } public HashSet KnownStationIds { get; } = new(StringComparer.Ordinal); public List History { get; } = []; public string? ActiveOrderId { get; set; } public int ActiveSubTaskIndex { get; set; } public List ActiveSubTasks { get; } = []; public string LastSignature { get; set; } = string.Empty; public string LastDeltaSignature { get; set; } = string.Empty; } public sealed class ShipOrderQueue : IReadOnlyList { public const int MaxOrders = 8; private readonly List _orders = []; public int Count => _orders.Count; public ShipOrderRuntime this[int index] => _orders[index]; public IEnumerator GetEnumerator() => _orders.GetEnumerator(); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); public void Enqueue(ShipOrderRuntime order) { if (_orders.Count >= MaxOrders) { throw new InvalidOperationException("Order queue is full."); } _orders.Add(order); } public void EnqueuePlayerOrder(ShipOrderRuntime order) { if (order.SourceKind != ShipOrderSourceKind.Player) { throw new InvalidOperationException("Player segment only accepts player orders."); } EnsureCapacityForNewOrder(order.Id); _orders.Insert(GetManagedInsertionIndex(), order); } public void EnqueueManagedOrder(ShipOrderRuntime order) { EnsureCapacityForNewOrder(order.Id); _orders.Add(order); } public void AddOrReplaceManagedOrder(ShipOrderRuntime order) => AddOrReplaceManagedOrder(order, insertAtFront: false); public void AddOrReplaceManagedOrderAtFront(ShipOrderRuntime order) => AddOrReplaceManagedOrder(order, insertAtFront: true); private void AddOrReplaceManagedOrder(ShipOrderRuntime order, bool insertAtFront) { var existingIndex = _orders.FindIndex(candidate => string.Equals(candidate.Id, order.Id, StringComparison.Ordinal)); if (existingIndex >= 0) { _orders[existingIndex] = order; return; } EnsureCapacityForNewOrder(order.Id); if (insertAtFront) { _orders.Insert(GetManagedInsertionIndex(), order); return; } _orders.Add(order); } public bool Remove(ShipOrderRuntime order) => RemoveById(order.Id); public bool RemoveById(string orderId) => _orders.RemoveAll(order => string.Equals(order.Id, orderId, StringComparison.Ordinal)) > 0; public int RemoveWhere(Predicate predicate) => _orders.RemoveAll(predicate); public ShipOrderRuntime? FindById(string orderId) => _orders.FirstOrDefault(order => string.Equals(order.Id, orderId, StringComparison.Ordinal)); public ShipOrderRuntime? FindLeadingOrderForSource(ShipOrderSourceKind sourceKind) => _orders.FirstOrDefault(order => order.SourceKind == sourceKind); public string? GetLeadingOrderLabelForSource(ShipOrderSourceKind sourceKind) => FindLeadingOrderForSource(sourceKind) is { } order ? order.Label ?? order.Kind : null; public bool HasOrdersFromSource(ShipOrderSourceKind sourceKind) => _orders.Any(order => order.SourceKind == sourceKind); public ShipOrderRuntime? GetCurrentOrder() => _orders.FirstOrDefault(order => order.Status is OrderStatus.Queued or OrderStatus.Active); public bool TryMovePlayerOrder(string orderId, int targetIndex) { var currentIndex = _orders.FindIndex(order => string.Equals(order.Id, orderId, StringComparison.Ordinal)); if (currentIndex < 0) { return false; } var order = _orders[currentIndex]; if (order.SourceKind != ShipOrderSourceKind.Player) { return false; } var playerOrderIds = _orders .Select((candidate, index) => (candidate, index)) .Where(entry => entry.candidate.SourceKind == ShipOrderSourceKind.Player) .Select(entry => entry.index) .ToList(); if (playerOrderIds.Count <= 1) { return true; } var clampedPlayerIndex = Math.Clamp(targetIndex, 0, playerOrderIds.Count - 1); var destinationIndex = playerOrderIds[clampedPlayerIndex]; if (currentIndex == destinationIndex) { return true; } _orders.RemoveAt(currentIndex); if (currentIndex < destinationIndex) { destinationIndex -= 1; } _orders.Insert(destinationIndex, order); return true; } public bool TryCompleteOrder(string orderId) => TryTransitionOrder(orderId, OrderStatus.Completed); public bool TryFailOrder(string orderId, string? failureReason = null) { var order = FindById(orderId); if (order is null) { return false; } order.FailureReason = failureReason ?? order.FailureReason; if (order.SourceKind == ShipOrderSourceKind.Player) { order.Status = OrderStatus.Failed; return true; } return TryTransitionOrder(orderId, OrderStatus.Failed); } public bool TryTransitionOrder(string orderId, OrderStatus terminalStatus) { var order = FindById(orderId); if (order is null) { return false; } order.Status = terminalStatus; return RemoveById(orderId); } private int GetManagedInsertionIndex() => _orders.TakeWhile(order => order.SourceKind == ShipOrderSourceKind.Player).Count(); private void EnsureCapacityForNewOrder(string orderId) { if (FindById(orderId) is not null) { return; } if (_orders.Count >= MaxOrders) { throw new InvalidOperationException("Order queue is full."); } } } public sealed class ShipSkillProfileRuntime { public int Navigation { get; set; } public int Trade { get; set; } public int Mining { get; set; } public int Combat { get; set; } public int Construction { get; set; } } public sealed class ShipOrderRuntime { public required string Id { get; init; } public required string Kind { get; init; } public required ShipOrderSourceKind SourceKind { get; init; } public required string SourceId { get; init; } public OrderStatus Status { get; set; } = OrderStatus.Queued; public int Priority { get; set; } public bool InterruptCurrentPlan { get; set; } = true; public DateTimeOffset CreatedAtUtc { get; init; } = DateTimeOffset.UtcNow; public string? Label { get; set; } public string? TargetEntityId { get; set; } public string? TargetSystemId { get; set; } public Vector3? TargetPosition { get; set; } public string? SourceStationId { get; set; } public string? DestinationStationId { get; set; } public string? ItemId { get; set; } public string? AnchorId { get; set; } public string? ConstructionSiteId { get; set; } public string? ModuleId { get; set; } public float WaitSeconds { get; set; } public float Radius { get; set; } public int? MaxSystemRange { get; set; } public bool KnownStationsOnly { get; set; } public string? FailureReason { get; set; } } public sealed class DefaultBehaviorRuntime { public required string Kind { get; set; } public string? HomeSystemId { get; set; } public string? HomeStationId { get; set; } public string? AreaSystemId { get; set; } public string? TargetEntityId { get; set; } public string? ItemId { get; set; } public string? PreferredAnchorId { get; set; } public string? PreferredConstructionSiteId { get; set; } public string? PreferredModuleId { get; set; } public Vector3? TargetPosition { get; set; } public float WaitSeconds { get; set; } = 3f; public float Radius { get; set; } = 24f; public int MaxSystemRange { get; set; } public bool KnownStationsOnly { get; set; } public List PatrolPoints { get; set; } = []; public int PatrolIndex { get; set; } public List RepeatOrders { get; set; } = []; public int RepeatIndex { get; set; } } public sealed class ShipOrderTemplateRuntime { public required string Kind { get; init; } public string? Label { get; set; } public string? TargetEntityId { get; set; } public string? TargetSystemId { get; set; } public Vector3? TargetPosition { get; set; } public string? SourceStationId { get; set; } public string? DestinationStationId { get; set; } public string? ItemId { get; set; } public string? AnchorId { get; set; } public string? ConstructionSiteId { get; set; } public string? ModuleId { get; set; } public float WaitSeconds { get; set; } public float Radius { get; set; } public int? MaxSystemRange { get; set; } public bool KnownStationsOnly { get; set; } } public sealed class ShipSubTaskRuntime { public required string Id { get; init; } public required string Kind { get; init; } public required string Summary { get; set; } public WorkStatus Status { get; set; } = WorkStatus.Pending; public string? TargetEntityId { get; set; } public string? TargetSystemId { get; set; } public string? TargetAnchorId { get; set; } public string? TargetResourceNodeId { get; set; } public string? TargetResourceDepositId { get; set; } public Vector3? TargetPosition { get; set; } public string? ItemId { get; set; } public string? ModuleId { get; set; } public float Threshold { get; set; } public float Amount { get; set; } public float ElapsedSeconds { get; set; } public float TotalSeconds { get; set; } public float Progress { get; set; } public string? BlockingReason { get; set; } }