Aetheria/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs
2026-07-17 06:40:51 +01:00

2437 lines
111 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
#nullable enable
namespace GameCult.Aetheria.State.Verse
{
public sealed class AetheriaRuntimeDaemonOperationResult
{
public AetheriaRuntimeDaemonOperationResult(
AetheriaRuntimeRunCheckpointCommit run,
IReadOnlyList<string> appliedCommandIds,
IReadOnlyList<string> rejectedCommandIds,
AetheriaRuntimeDaemonIntentState? intents = null,
IReadOnlyDictionary<string, string>? rejectedCommandReasons = null)
{
Run = run ?? new AetheriaRuntimeRunCheckpointCommit();
AppliedCommandIds = appliedCommandIds ?? Array.Empty<string>();
RejectedCommandIds = rejectedCommandIds ?? Array.Empty<string>();
Intents = intents ?? new AetheriaRuntimeDaemonIntentState();
RejectedCommandReasons = rejectedCommandReasons ?? new Dictionary<string, string>();
}
public AetheriaRuntimeRunCheckpointCommit Run { get; }
public IReadOnlyList<string> AppliedCommandIds { get; }
public IReadOnlyList<string> RejectedCommandIds { get; }
public AetheriaRuntimeDaemonIntentState Intents { get; }
public IReadOnlyDictionary<string, string> RejectedCommandReasons { get; }
}
public sealed class AetheriaRuntimeDaemonOperationContext
{
public const double DefaultDockingDistance = 25.0;
public const double DefaultWormholeExitRadius = 50.0;
public IReadOnlyList<AetheriaRuntimeLoadoutTemplateCommit> LoadoutTemplates { get; set; } =
Array.Empty<AetheriaRuntimeLoadoutTemplateCommit>();
public AetheriaRuntimeDaemonIntentState Intents { get; set; } = new AetheriaRuntimeDaemonIntentState();
public double DockingDistance { get; set; } = AetheriaRuntimeDaemonOperationContext.DefaultDockingDistance;
public double WormholeExitRadius { get; set; } = AetheriaRuntimeDaemonOperationContext.DefaultWormholeExitRadius;
public double WormholeDistanceRatio { get; set; } =
AetheriaRuntimeDaemonRenderSettings.AetheriaDefault.WormholeDistanceRatio;
public AetheriaRuntimeCatalogSnapshot? Catalog { get; set; }
internal string RejectionReason { get; private set; } = "";
internal void ResetRejectionReason() => RejectionReason = "";
internal bool Reject(string reason)
{
RejectionReason = reason ?? "";
return false;
}
}
public static class AetheriaRuntimeDaemonRejectionReasons
{
public const string InvalidCommandState = "invalid-command-state";
public const string RunTerminal = "run-terminal";
public const string AuthorityDenied = "authority-denied";
public const string InvalidDockActor = "invalid-dock-actor";
public const string InvalidDockTarget = "invalid-dock-target";
public const string AlreadyDocked = "already-docked";
public const string NoEligibleDockingBay = "no-eligible-docking-bay";
public const string NotDocked = "not-docked";
public const string MissingCockpit = "missing-cockpit";
public const string MissingPropulsion = "missing-propulsion";
public const string MissingReactor = "missing-reactor";
public const string DockingBayCargoNotEmpty = "docking-bay-cargo-not-empty";
public const string RefitRequiresDocked = "refit-requires-docked";
public const string RefitAccessDenied = "refit-access-denied";
public const string InvalidRefitSource = "invalid-refit-source";
public const string InvalidRefitItem = "invalid-refit-item";
public const string RefitItemMustBeSingle = "refit-item-must-be-single";
public const string RefitBayNotEmpty = "refit-bay-not-empty";
public const string RefitNoFit = "refit-no-fit";
public const string InvalidCargoDestination = "invalid-cargo-destination";
public const string InvalidCargoSource = "invalid-cargo-source";
public const string CargoAccessDenied = "cargo-access-denied";
public const string CargoNoFit = "cargo-no-fit";
public const string CargoStackLimit = "cargo-stack-limit";
public const string TradeRequiresDocked = "trade-requires-docked";
public const string InvalidTradeItem = "invalid-trade-item";
public const string InvalidTradeSource = "invalid-trade-source";
public const string TradeStockUnavailable = "trade-stock-unavailable";
public const string TradeInsufficientCredits = "trade-insufficient-credits";
public const string TradeTargetNoFit = "trade-target-no-fit";
public const string TradeStationNoFit = "trade-station-no-fit";
public const string InvalidTradePayout = "invalid-trade-payout";
}
public static class AetheriaRuntimeDaemonOperations
{
public static AetheriaRuntimeDaemonOperationResult Execute(
AetheriaRuntimeRunCheckpointCommit run,
IEnumerable<AetheriaRuntimeDaemonCommandDocument> commands)
{
return Execute(run, commands, new AetheriaRuntimeDaemonOperationContext());
}
public static AetheriaRuntimeDaemonOperationResult Execute(
AetheriaRuntimeRunCheckpointCommit run,
IEnumerable<AetheriaRuntimeDaemonCommandDocument> commands,
AetheriaRuntimeDaemonOperationContext context)
{
run ??= new AetheriaRuntimeRunCheckpointCommit();
context ??= new AetheriaRuntimeDaemonOperationContext();
context.Intents = new AetheriaRuntimeDaemonIntentState();
var applied = new List<string>();
var rejected = new List<string>();
var rejectedReasons = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var command in commands ?? Enumerable.Empty<AetheriaRuntimeDaemonCommandDocument>())
{
if (command == null || string.IsNullOrWhiteSpace(command.CommandId))
continue;
if (!AetheriaRuntimeRunLifecycle.IsActive(run))
{
rejected.Add(command.CommandId);
rejectedReasons[command.CommandId] = AetheriaRuntimeDaemonRejectionReasons.RunTerminal;
continue;
}
context.ResetRejectionReason();
if (ApplyOne(run, command, context))
applied.Add(command.CommandId);
else
{
rejected.Add(command.CommandId);
rejectedReasons[command.CommandId] = string.IsNullOrWhiteSpace(context.RejectionReason)
? AetheriaRuntimeDaemonRejectionReasons.InvalidCommandState
: context.RejectionReason;
}
}
return new AetheriaRuntimeDaemonOperationResult(
run, applied, rejected, context.Intents, rejectedReasons);
}
private static bool ApplyOne(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context)
{
switch (command.Kind)
{
case AetheriaRuntimeDaemonCommandKinds.SetTarget:
return ApplySetTarget(run, command);
case AetheriaRuntimeDaemonCommandKinds.ClearTarget:
return ApplyClearTarget(run, command);
case AetheriaRuntimeDaemonCommandKinds.TargetNearest:
return ApplyTargetCycle(run, command, TargetCycleMode.Nearest);
case AetheriaRuntimeDaemonCommandKinds.TargetNext:
return ApplyTargetCycle(run, command, TargetCycleMode.Next);
case AetheriaRuntimeDaemonCommandKinds.TargetPrevious:
return ApplyTargetCycle(run, command, TargetCycleMode.Previous);
case AetheriaRuntimeDaemonCommandKinds.TargetReticle:
return ApplyTargetReticle(run, command);
case AetheriaRuntimeDaemonCommandKinds.SetLookDirection:
return ApplySetLookDirection(run, command);
case AetheriaRuntimeDaemonCommandKinds.SetTractorPower:
if (!IsNormalizedScalar(command.ScalarValue))
return false;
return ApplyCurrentEntity(run, command, entity =>
entity.TractorTargetPower = command.ScalarValue);
case AetheriaRuntimeDaemonCommandKinds.SetHeatsinksEnabled:
return ApplyCurrentEntity(run, command, entity =>
entity.HeatsinksEnabled = command.ScalarValue > 0.5);
case AetheriaRuntimeDaemonCommandKinds.SetOverrideShutdown:
return ApplyTargetOrCurrentEntity(run, command, entity =>
entity.OverrideShutdown = command.ScalarValue > 0.5);
case AetheriaRuntimeDaemonCommandKinds.SetShutdownPerformance:
if (command.ScalarValue < 0.0 || command.ScalarValue > 1.0)
return false;
return ApplyTargetEntity(run, command.TargetEntityKey, entity =>
entity.ShutdownPerformance = command.ScalarValue);
case AetheriaRuntimeDaemonCommandKinds.SetItemEnabled:
return ApplyCurrentEquipmentItem(run, command, item =>
item.Enabled = command.ScalarValue > 0.5);
case AetheriaRuntimeDaemonCommandKinds.ToggleShieldEnabled:
return ApplyToggleEquipmentBehaviorItem(run, command, "Shield", item =>
item.Enabled = !item.Enabled);
case AetheriaRuntimeDaemonCommandKinds.SetItemOverrideShutdown:
return ApplyTargetEquipmentItem(run, command, item =>
item.OverrideShutdown = command.ScalarValue > 0.5);
case AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupMembership:
return ApplySetWeaponGroupMembership(run, command);
case AetheriaRuntimeDaemonCommandKinds.SetThermotoggleTargetTemperature:
return ApplySetThermotoggleTargetTemperature(run, command, context);
case AetheriaRuntimeDaemonCommandKinds.SetEntityName:
return ApplyTargetEntity(run, command.TargetEntityKey, entity =>
entity.Name = command.TextValue ?? "");
case AetheriaRuntimeDaemonCommandKinds.SetDockedCurrentShip:
return ApplySetDockedCurrentShip(run, command.TargetEntityKey);
case AetheriaRuntimeDaemonCommandKinds.TransferCargoItem:
return ApplyTransferCargoItem(run, command, context);
case AetheriaRuntimeDaemonCommandKinds.EquipItem:
return ApplyEquipItem(run, command, context);
case AetheriaRuntimeDaemonCommandKinds.StoreItem:
return ApplyStoreItem(run, command, context);
case AetheriaRuntimeDaemonCommandKinds.ToggleHullConductivity:
return ApplyToggleHullConductivity(run, command);
case AetheriaRuntimeDaemonCommandKinds.TradePurchase:
return ApplyTradePurchase(run, command, context);
case AetheriaRuntimeDaemonCommandKinds.TradeSale:
return ApplyTradeSale(run, command, context);
case AetheriaRuntimeDaemonCommandKinds.RestoreLoadout:
return ApplyRestoreLoadout(run, command, context);
case AetheriaRuntimeDaemonCommandKinds.SetMoveVector:
return ApplySetMoveVector(run, command, context.Intents);
case AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup:
return ApplyFireWeaponGroup(run, command, context.Intents);
case AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupActive:
return ApplySetWeaponGroupActive(run, command, context.Intents);
case AetheriaRuntimeDaemonCommandKinds.SetBehaviorActive:
return ApplySetBehaviorActive(run, command, context.Intents);
case AetheriaRuntimeDaemonCommandKinds.ActivateConsumable:
return ApplyActivateConsumable(run, command, context.Intents);
case AetheriaRuntimeDaemonCommandKinds.SensorPing:
return ApplySensorPing(run, command, context.Intents);
case AetheriaRuntimeDaemonCommandKinds.Dock:
return ApplyDockIntent(run, command, context, context.DockingDistance);
case AetheriaRuntimeDaemonCommandKinds.DockNearest:
return ApplyDockNearestIntent(run, command, context, context.DockingDistance);
case AetheriaRuntimeDaemonCommandKinds.Undock:
return ApplyUndockIntent(run, command, context);
case AetheriaRuntimeDaemonCommandKinds.Interact:
return ApplyInteractIntent(
run,
command,
context,
context.DockingDistance,
context.WormholeExitRadius,
context.WormholeDistanceRatio);
case AetheriaRuntimeDaemonCommandKinds.EnterWormhole:
return ApplyEnterWormholeIntent(run, command, context.Intents,
context.WormholeExitRadius, context.WormholeDistanceRatio);
case AetheriaRuntimeDaemonCommandKinds.TowToStation:
return ApplyTowToStation(run, command, context.DockingDistance);
case AetheriaRuntimeDaemonCommandKinds.IssueAgentTask:
return ApplyIssueAgentTask(run, command);
case AetheriaRuntimeDaemonCommandKinds.CancelAgentTask:
return ApplyCancelAgentTask(run, command);
case AetheriaRuntimeDaemonCommandKinds.SetSimulationRate:
return IsSupportedSimulationRate(command.ScalarValue);
case AetheriaRuntimeDaemonCommandKinds.AdvanceSimulationStep:
return true;
default:
return false;
}
}
public static readonly double[] SupportedSimulationRates = { 0, 0.25, 0.5, 1, 2, 4, 8, 16, 32, 64, 128 };
public static bool IsSupportedSimulationRate(double value) =>
SupportedSimulationRates.Any(rate => Math.Abs(value - rate) < 0.000001);
public static bool RequiresSimulationStep(AetheriaRuntimeDaemonCommandKinds kind)
{
switch (kind)
{
case AetheriaRuntimeDaemonCommandKinds.SetMoveVector:
case AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup:
case AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupActive:
case AetheriaRuntimeDaemonCommandKinds.SetBehaviorActive:
case AetheriaRuntimeDaemonCommandKinds.ActivateConsumable:
case AetheriaRuntimeDaemonCommandKinds.SensorPing:
case AetheriaRuntimeDaemonCommandKinds.Dock:
case AetheriaRuntimeDaemonCommandKinds.DockNearest:
case AetheriaRuntimeDaemonCommandKinds.Undock:
case AetheriaRuntimeDaemonCommandKinds.Interact:
case AetheriaRuntimeDaemonCommandKinds.EnterWormhole:
return true;
default:
return false;
}
}
private static bool ApplyIssueAgentTask(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command)
{
var request = command.AgentTask ?? new AetheriaRuntimeAgentTaskCommand();
var taskId = string.IsNullOrWhiteSpace(request.TaskId) ? command.CommandId : request.TaskId;
var taskType = (request.TaskType ?? "").Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(taskId) ||
string.IsNullOrWhiteSpace(request.CorporationKey) ||
!AetheriaRuntimeAgentTaskTypes.All.Contains(taskType, StringComparer.Ordinal) ||
request.ZoneIndex < 0 ||
!(run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>()).Any(zone =>
zone != null && zone.ZoneIndex == request.ZoneIndex) ||
(run.AgentTasks ?? Array.Empty<AetheriaRuntimeAgentTaskCommit>()).Any(task =>
task != null && string.Equals(task.TaskId, taskId, StringComparison.Ordinal)))
{
return false;
}
if (string.Equals(taskType, AetheriaRuntimeAgentTaskTypes.Haul, StringComparison.Ordinal) &&
(request.OriginEntityIndex < 0 || request.TargetEntityIndex < 0 ||
string.IsNullOrWhiteSpace(request.ItemKey) || request.Quantity <= 0))
{
return false;
}
if (string.Equals(taskType, AetheriaRuntimeAgentTaskTypes.Defend, StringComparison.Ordinal) &&
!(request.TargetBodyKeys ?? Array.Empty<string>()).Any(key => !string.IsNullOrWhiteSpace(key)))
{
return false;
}
if (string.Equals(taskType, AetheriaRuntimeAgentTaskTypes.Tow, StringComparison.Ordinal) &&
(request.TargetEntityIndex < 0 || string.IsNullOrWhiteSpace(request.OrbitParentKey) || request.OrbitDistance <= 0))
return false;
if (string.Equals(taskType, AetheriaRuntimeAgentTaskTypes.Attack, StringComparison.Ordinal) &&
!CanIssueAttackTask(run, request))
return false;
run.AgentTasks = (run.AgentTasks ?? Array.Empty<AetheriaRuntimeAgentTaskCommit>())
.Concat(new[]
{
new AetheriaRuntimeAgentTaskCommit
{
TaskId = taskId,
CorporationKey = request.CorporationKey ?? "",
TaskType = taskType,
Priority = request.Priority,
ZoneIndex = request.ZoneIndex,
TargetEntityIndex = request.TargetEntityIndex,
TargetPositionX = request.TargetPositionX,
TargetPositionZ = request.TargetPositionZ,
CompletionRadius = request.CompletionRadius > 0 ? request.CompletionRadius : 10,
WeaponGroup = request.WeaponGroup,
OriginEntityIndex = request.OriginEntityIndex,
ItemKey = request.ItemKey ?? "",
RequestedQuantity = request.Quantity,
Phase = string.Equals(taskType, AetheriaRuntimeAgentTaskTypes.Haul, StringComparison.Ordinal)
? "pickup"
: "",
TargetBodyKeys = request.TargetBodyKeys ?? Array.Empty<string>(),
OrbitParentKey = request.OrbitParentKey ?? "",
OrbitDistance = request.OrbitDistance,
Status = AetheriaRuntimeAgentTaskStatuses.Queued
}
})
.ToArray();
return true;
}
private static bool CanIssueAttackTask(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeAgentTaskCommand request)
{
if (request.TargetEntityIndex < 0 || request.WeaponGroup < 0)
return false;
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == request.ZoneIndex);
var target = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.EntityIndex == request.TargetEntityIndex);
if (target == null || !target.IsActive || !IsAttackHostile(request.CorporationKey, target.FactionKey))
return false;
return (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.Where(candidate => candidate != null)
.SelectMany(candidate => (candidate.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
.Select(entity => (ZoneIndex: candidate.ZoneIndex, Entity: entity)))
.Any(candidate =>
candidate.Entity != null &&
candidate.Entity.IsActive &&
!IsCurrentEntity(run, candidate.ZoneIndex, candidate.Entity.EntityIndex) &&
string.Equals(candidate.Entity.FactionKey ?? "", request.CorporationKey ?? "", StringComparison.Ordinal) &&
(candidate.Entity.AgentTaskCapabilities ?? Array.Empty<string>())
.Contains(AetheriaRuntimeAgentTaskTypes.Attack, StringComparer.Ordinal) &&
request.WeaponGroup < (candidate.Entity.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>()).Count &&
(candidate.Entity.WeaponGroups[request.WeaponGroup] ?? Array.Empty<int>()).Count > 0);
}
private static bool IsAttackHostile(string? corporationKey, string? targetFactionKey)
{
var corporationIsPlayer = string.Equals(corporationKey, "player", StringComparison.OrdinalIgnoreCase);
var targetIsPlayer = string.Equals(targetFactionKey, "player", StringComparison.OrdinalIgnoreCase);
return corporationIsPlayer != targetIsPlayer &&
(string.Equals(corporationKey, "raider", StringComparison.OrdinalIgnoreCase) ||
string.Equals(targetFactionKey, "raider", StringComparison.OrdinalIgnoreCase));
}
private static bool IsCurrentEntity(
AetheriaRuntimeRunCheckpointCommit run,
int zoneIndex,
int entityIndex)
{
return AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(
run.CurrentEntityKey,
out var currentZoneIndex,
out var currentEntityIndex) &&
currentZoneIndex == zoneIndex &&
currentEntityIndex == entityIndex;
}
private static bool ApplyCancelAgentTask(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command)
{
var taskId = command.AgentTask?.TaskId ?? command.TextValue ?? "";
var task = (run.AgentTasks ?? Array.Empty<AetheriaRuntimeAgentTaskCommit>())
.FirstOrDefault(candidate => candidate != null && string.Equals(candidate.TaskId, taskId, StringComparison.Ordinal));
if (task == null ||
string.Equals(task.Status, AetheriaRuntimeAgentTaskStatuses.Completed, StringComparison.Ordinal) ||
string.Equals(task.Status, AetheriaRuntimeAgentTaskStatuses.Cancelled, StringComparison.Ordinal))
{
return false;
}
foreach (var entity in (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.Where(zone => zone != null)
.SelectMany(zone => zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()))
{
if (string.Equals(entity.AssignedAgentTaskId, task.TaskId, StringComparison.Ordinal))
AetheriaRuntimeAgentScheduler.ReleaseControls(entity, task);
}
task.Status = AetheriaRuntimeAgentTaskStatuses.Cancelled;
task.AssignedEntityIndex = -1;
task.PendingQuantity = 0;
task.Phase = AetheriaRuntimeAgentTaskStatuses.Cancelled;
return true;
}
private static bool ApplySetTarget(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command)
{
var actorKey = string.IsNullOrWhiteSpace(command.ActorEntityKey)
? run.CurrentEntityKey
: command.ActorEntityKey;
if (!TryResolveEntity(run, actorKey, out var actorZone, out var actorIndex, out var actor) ||
!TryResolveEntity(run, command.TargetEntityKey, out var targetZone, out var targetIndex, out _) ||
actorZone != targetZone ||
actorIndex == targetIndex)
{
return false;
}
if (!(actor.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
.Any(contact => contact != null && contact.TargetEntityIndex == targetIndex && contact.Visible))
{
return false;
}
actor.TargetEntityIndex = targetIndex;
return true;
}
private enum TargetCycleMode
{
Nearest,
Next,
Previous
}
private static bool ApplyTargetCycle(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
TargetCycleMode mode)
{
var actorKey = string.IsNullOrWhiteSpace(command.ActorEntityKey)
? run.CurrentEntityKey
: command.ActorEntityKey;
if (!TryResolveEntity(run, actorKey, out var actorZone, out var actorIndex, out var actor))
return false;
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == actorZone);
if (zone == null)
return false;
var orderedTargets = VisibleHostileTargets(zone, actor, actorIndex)
.OrderBy(target => target.DistanceSq)
.ThenBy(target => target.EntityIndex)
.ToArray();
if (orderedTargets.Length == 0)
return false;
if (mode == TargetCycleMode.Nearest)
{
actor.TargetEntityIndex = orderedTargets[0].EntityIndex;
return true;
}
var currentIndex = Array.FindIndex(
orderedTargets,
target => target.EntityIndex == actor.TargetEntityIndex);
if (currentIndex < 0)
{
actor.TargetEntityIndex = orderedTargets[0].EntityIndex;
return true;
}
var nextIndex = mode == TargetCycleMode.Next
? (currentIndex + 1 + orderedTargets.Length) % orderedTargets.Length
: (currentIndex - 1 + orderedTargets.Length) % orderedTargets.Length;
actor.TargetEntityIndex = orderedTargets[nextIndex].EntityIndex;
return true;
}
private static bool ApplyTargetReticle(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command)
{
var actorKey = string.IsNullOrWhiteSpace(command.ActorEntityKey)
? run.CurrentEntityKey
: command.ActorEntityKey;
if (!TryResolveEntity(run, actorKey, out var actorZone, out var actorIndex, out var actor))
return false;
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == actorZone);
if (zone == null)
return false;
var lookLength = Math.Sqrt(
(command.DirectionX * command.DirectionX) +
(command.DirectionY * command.DirectionY) +
(command.PositionZ * command.PositionZ));
if (lookLength <= double.Epsilon)
return false;
var lookX = command.DirectionX / lookLength;
var lookY = command.DirectionY / lookLength;
var lookZ = command.PositionZ / lookLength;
var scoredTargets = VisibleHostileTargets(zone, actor, actorIndex)
.Select(candidate =>
{
var distance = Math.Sqrt(candidate.DistanceSq);
var dot = distance <= double.Epsilon
? double.NegativeInfinity
: ((candidate.DeltaX / distance) * lookX) +
((candidate.DeltaY / distance) * lookY) +
((candidate.DeltaZ / distance) * lookZ);
return (candidate.EntityIndex, Dot: dot);
})
.Where(candidate => candidate.Dot > double.NegativeInfinity)
.OrderByDescending(candidate => candidate.Dot)
.ThenBy(candidate => candidate.EntityIndex)
.ToArray();
if (scoredTargets.Length == 0)
return false;
var target = scoredTargets[0];
actor.TargetEntityIndex = actor.TargetEntityIndex == target.EntityIndex
? -1
: target.EntityIndex;
return true;
}
private static IEnumerable<(int EntityIndex, double DistanceSq, double DeltaX, double DeltaY, double DeltaZ)> VisibleHostileTargets(
AetheriaRuntimeZoneSnapshotCommit zone,
AetheriaRuntimeEntitySnapshotCommit actor,
int actorIndex)
{
var entities = zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>();
foreach (var contact in actor.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
{
if (contact == null ||
!contact.Visible ||
!contact.Hostile ||
contact.TargetEntityIndex == actorIndex ||
contact.TargetEntityIndex < 0 ||
contact.TargetEntityIndex >= entities.Count)
{
continue;
}
var target = entities[contact.TargetEntityIndex];
if (target == null)
continue;
var deltaX = target.PositionX - actor.PositionX;
var deltaY = target.PositionY - actor.PositionY;
var deltaZ = target.PositionZ - actor.PositionZ;
yield return (
contact.TargetEntityIndex,
(deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ),
deltaX,
deltaY,
deltaZ);
}
}
private static bool ApplyClearTarget(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command)
{
return ApplyCurrentEntity(run, command, entity => entity.TargetEntityIndex = -1);
}
private static bool ApplySetLookDirection(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command)
{
if (!IsFinite(command.DirectionX) || !IsFinite(command.PositionZ))
return false;
var length = Math.Sqrt(command.DirectionX * command.DirectionX + command.PositionZ * command.PositionZ);
if (length <= 0.000001)
return false;
return ApplyCurrentEntity(run, command, entity =>
{
entity.LookDirectionX = command.DirectionX / length;
entity.LookDirectionY = command.PositionZ / length;
});
}
private static bool ApplySetCurrentEntity(
AetheriaRuntimeRunCheckpointCommit run,
string entityKey)
{
if (!TryResolveEntity(run, entityKey, out var zoneIndex, out var entityIndex, out _))
return false;
run.CurrentZoneIndex = zoneIndex;
run.CurrentEntityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run.RunId, zoneIndex, entityIndex);
return true;
}
private static bool ApplySetDockedCurrentShip(
AetheriaRuntimeRunCheckpointCommit run,
string entityKey)
{
if (!TryResolveEntity(run, entityKey, out var zoneIndex, out var entityIndex, out _))
return false;
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == zoneIndex);
if (zone == null)
return false;
var isDocked = (zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
.Any(entity =>
entity != null &&
entity.EntityIndex != entityIndex &&
(entity.DockingBayAssignments ?? Array.Empty<int>()).Contains(entityIndex));
if (!isDocked)
return false;
run.CurrentZoneIndex = zoneIndex;
run.CurrentEntityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run.RunId, zoneIndex, entityIndex);
return true;
}
private static bool ApplyTargetOrCurrentEntity(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
Action<AetheriaRuntimeEntitySnapshotCommit> mutate)
{
return string.IsNullOrWhiteSpace(command.TargetEntityKey)
? ApplyCurrentEntity(run, command, mutate)
: ApplyTargetEntity(run, command.TargetEntityKey, mutate);
}
private static bool ApplyCurrentEntity(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
Action<AetheriaRuntimeEntitySnapshotCommit> mutate)
{
var entityKey = string.IsNullOrWhiteSpace(command.ActorEntityKey)
? run.CurrentEntityKey
: command.ActorEntityKey;
return ApplyTargetEntity(run, entityKey, mutate);
}
private static bool ApplyCurrentEquipmentItem(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
Action<AetheriaRuntimeLoadoutItemCommit> mutate)
{
var entityKey = string.IsNullOrWhiteSpace(command.TargetEntityKey)
? command.ActorEntityKey
: command.TargetEntityKey;
if (string.IsNullOrWhiteSpace(entityKey))
entityKey = run.CurrentEntityKey;
return ApplyTargetEquipmentItem(run, entityKey, command.EquipmentIndex, mutate);
}
private static bool ApplyTargetEquipmentItem(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
Action<AetheriaRuntimeLoadoutItemCommit> mutate)
{
return ApplyTargetEquipmentItem(run, command.TargetEntityKey, command.EquipmentIndex, mutate);
}
private static bool ApplyTargetEquipmentItem(
AetheriaRuntimeRunCheckpointCommit run,
string entityKey,
int equipmentIndex,
Action<AetheriaRuntimeLoadoutItemCommit> mutate)
{
if (!TryResolveEntity(run, entityKey, out _, out _, out var entity))
return false;
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
if (equipmentIndex < 0 || equipmentIndex >= equipment.Count)
return false;
var item = equipment[equipmentIndex]?.Item;
if (item == null)
return false;
mutate(item);
return true;
}
private static bool ApplySetWeaponGroupMembership(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command)
{
if (!TryResolveEntity(run, command.TargetEntityKey, out _, out _, out var entity) ||
command.EquipmentIndex < 0 ||
command.WeaponGroup < 0)
{
return false;
}
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
if (command.EquipmentIndex >= equipment.Count ||
equipment[command.EquipmentIndex]?.Item == null)
{
return false;
}
var sourceGroups = entity.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>();
var groupCount = Math.Max(sourceGroups.Count, command.WeaponGroup + 1);
var groups = new IReadOnlyList<int>[groupCount];
for (var i = 0; i < groupCount; i++)
{
groups[i] = i < sourceGroups.Count
? (sourceGroups[i] ?? Array.Empty<int>()).ToArray()
: Array.Empty<int>();
}
var members = groups[command.WeaponGroup]
.Where(index => index != command.EquipmentIndex)
.ToList();
if (command.ScalarValue > 0.5)
members.Add(command.EquipmentIndex);
groups[command.WeaponGroup] = members
.Distinct()
.OrderBy(index => index)
.ToArray();
entity.WeaponGroups = groups;
return true;
}
private static bool ApplySetThermotoggleTargetTemperature(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context)
{
if (!double.IsFinite(command.ScalarValue) ||
!TryResolveEntity(run, command.TargetEntityKey, out _, out _, out var entity))
return false;
var behavior = (entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
.FirstOrDefault(candidate =>
candidate != null &&
string.Equals(candidate.OwnerKind, "equipment", StringComparison.Ordinal) &&
candidate.OwnerIndex == command.EquipmentIndex &&
candidate.BehaviorIndex == command.BehaviorIndex);
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
var item = command.EquipmentIndex >= 0 && command.EquipmentIndex < equipment.Count
? equipment[command.EquipmentIndex]?.Item
: null;
var payloads = context.Catalog?.FindItem(item?.ItemKey ?? "")?.BehaviorPayloads ??
Array.Empty<AetheriaRuntimeBehaviorPayload>();
var payload = command.BehaviorIndex >= 0 && command.BehaviorIndex < payloads.Count
? payloads[command.BehaviorIndex]
: null;
var adjustable = (payload?.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
.FirstOrDefault(field => field != null && field.Key == 3)?.Value?.BoolValue ?? false;
if (behavior == null ||
!string.Equals(behavior.BehaviorKind, "Thermotoggle", StringComparison.Ordinal) ||
!string.Equals(payload?.Kind, "Thermotoggle", StringComparison.Ordinal) ||
!adjustable)
return false;
behavior.ThermotoggleTargetTemperature = command.ScalarValue;
return true;
}
private static bool ApplyToggleEquipmentBehaviorItem(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
string behaviorKind,
Action<AetheriaRuntimeLoadoutItemCommit> mutate)
{
var entityKey = string.IsNullOrWhiteSpace(command.TargetEntityKey)
? command.ActorEntityKey
: command.TargetEntityKey;
if (string.IsNullOrWhiteSpace(entityKey))
entityKey = run.CurrentEntityKey;
if (!TryResolveEntity(run, entityKey, out _, out _, out var entity))
return false;
var behavior = (entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
.FirstOrDefault(candidate =>
candidate != null &&
string.Equals(candidate.OwnerKind, "equipment", StringComparison.Ordinal) &&
string.Equals(candidate.BehaviorKind, behaviorKind, StringComparison.Ordinal));
if (behavior == null || behavior.OwnerIndex < 0)
return false;
var equipment = entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
if (behavior.OwnerIndex >= equipment.Count)
return false;
var item = equipment[behavior.OwnerIndex]?.Item;
if (item == null)
return false;
mutate(item);
return true;
}
private static bool ApplyTransferCargoItem(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context)
{
var transfer = command.CargoTransfer ?? new AetheriaRuntimeCargoTransferCommand();
var quantity = transfer.Quantity > 0
? transfer.Quantity
: (int)Math.Round(command.ScalarValue);
if (!TryResolveEntity(run, transfer.OriginEntityKey, out _, out _, out var origin) ||
!TryResolveEntity(run, transfer.DestinationEntityKey, out _, out _, out var destination))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidCargoSource);
if (!CanAccessCargoTransfer(run, command.ActorEntityKey, transfer.OriginEntityKey, origin,
transfer.DestinationEntityKey, destination))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.CargoAccessDenied);
return AetheriaRuntimeRefitTransactions.TryTransferCargo(
origin,
transfer.OriginCargoIndex,
transfer.SourceX,
transfer.SourceY,
destination,
transfer.DestinationCargoIndex,
command.TextValue ?? "",
quantity,
transfer.DestinationX,
transfer.DestinationY,
transfer.HasDestinationPosition,
context.Catalog,
out var reason)
|| context.Reject(reason);
}
private static bool CanAccessCargoTransfer(
AetheriaRuntimeRunCheckpointCommit run,
string actorEntityKey,
string originEntityKey,
AetheriaRuntimeEntitySnapshotCommit origin,
string destinationEntityKey,
AetheriaRuntimeEntitySnapshotCommit destination)
{
if (!TryResolveEntity(run, actorEntityKey, out var actorZoneIndex, out _, out var actor))
return false;
var actorIsOrigin = ReferenceEquals(actor, origin);
var actorIsDestination = ReferenceEquals(actor, destination);
if (!actorIsOrigin && !actorIsDestination)
return false;
var other = actorIsOrigin ? destination : origin;
var otherKey = actorIsOrigin ? destinationEntityKey : originEntityKey;
if (ReferenceEquals(actor, other) ||
(TryResolveDockParent(run, actorEntityKey, out _, out var actorParent) && ReferenceEquals(actorParent, other)) ||
(TryResolveDockParent(run, otherKey, out _, out var otherParent) && ReferenceEquals(otherParent, actor)))
return true;
if (!TryResolveEntity(run, otherKey, out var otherZoneIndex, out _, out _) ||
actorZoneIndex != otherZoneIndex)
return false;
var dx = actor.PositionX - other.PositionX;
var dz = actor.PositionZ - other.PositionZ;
var reach = CargoInteractionRadius(actor) + CargoInteractionRadius(other) + 10.0;
return dx * dx + dz * dz <= reach * reach;
}
private static double CargoInteractionRadius(AetheriaRuntimeEntitySnapshotCommit entity) =>
string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase) ? 48.0 : 20.0;
private static bool ApplyEquipItem(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context)
{
var transfer = command.EquipmentTransfer ?? new AetheriaRuntimeEquipmentTransferCommand();
var originKey = transfer.OriginEntityKey ?? "";
var destinationKey = transfer.DestinationEntityKey ?? "";
if (!TryResolveEntity(run, originKey, out _, out _, out var origin) ||
!TryResolveEntity(run, destinationKey, out _, out _, out var destination))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidRefitSource);
if (!TryResolveDockParent(run, destinationKey, out _, out var parent))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.RefitRequiresDocked);
var sourceKind = string.IsNullOrWhiteSpace(transfer.SourceKind)
? AetheriaRuntimeRefitSourceKinds.Cargo
: transfer.SourceKind;
var sourceIsCargo = string.Equals(sourceKind, AetheriaRuntimeRefitSourceKinds.Cargo, StringComparison.Ordinal);
if ((!sourceIsCargo && !ReferenceEquals(origin, destination)) ||
(sourceIsCargo &&
!ReferenceEquals(origin, destination) &&
!ReferenceEquals(origin, parent)))
{
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.RefitAccessDenied);
}
return AetheriaRuntimeRefitTransactions.TryEquip(
origin,
sourceKind,
transfer.OriginIndex,
transfer.SourceX,
transfer.SourceY,
destination,
command.TextValue ?? "",
transfer.DestinationX,
transfer.DestinationY,
transfer.HasDestinationPosition,
context.Catalog,
out var reason)
|| context.Reject(reason);
}
private static bool ApplyStoreItem(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context)
{
var store = command.StoreItem ?? new AetheriaRuntimeStoreItemCommand();
var originKey = store.OriginEntityKey ?? "";
var destinationKey = store.DestinationEntityKey ?? "";
if (!TryResolveEntity(run, originKey, out _, out _, out var origin) ||
!TryResolveEntity(run, destinationKey, out _, out _, out var destination))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidRefitSource);
if (!TryResolveDockParent(run, originKey, out _, out var parent))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.RefitRequiresDocked);
if (!ReferenceEquals(destination, origin) && !ReferenceEquals(destination, parent))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.RefitAccessDenied);
return AetheriaRuntimeRefitTransactions.TryStore(
origin,
string.IsNullOrWhiteSpace(store.SourceKind)
? AetheriaRuntimeRefitSourceKinds.Equipment
: store.SourceKind,
store.SourceEquipmentIndex,
destination,
store.DestinationCargoIndex,
command.TextValue ?? "",
store.DestinationX,
store.DestinationY,
store.HasDestinationPosition,
context.Catalog,
out var reason)
|| context.Reject(reason);
}
private static bool ApplyToggleHullConductivity(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command)
{
if (!TryResolveEntity(run, command.TargetEntityKey, out _, out _, out var entity))
return false;
var x = (int)command.PositionX;
var y = (int)command.PositionY;
var axis = (int)command.ScalarValue;
if (axis < 0 || axis > 1)
return false;
var gridName = axis == 0 ? "hull_conductivity_x" : "hull_conductivity_y";
var grids = (entity.StatGrids ?? Array.Empty<AetheriaRuntimeEntityStatGridCommit>()).ToArray();
var grid = grids.FirstOrDefault(candidate =>
candidate != null &&
string.Equals(candidate.Name ?? "", gridName, StringComparison.Ordinal));
if (grid == null ||
x < 0 ||
y < 0 ||
x >= grid.Width ||
y >= grid.Height)
{
return false;
}
var values = (grid.Values ?? Array.Empty<double>()).ToArray();
var index = y * grid.Width + x;
if (index < 0 || index >= values.Length)
return false;
values[index] = values[index] > 0.5 ? 0.0 : 1.0;
grid.Values = values;
entity.StatGrids = grids;
return true;
}
private static bool ApplyTradePurchase(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context)
{
var purchase = command.TradePurchase ?? new AetheriaRuntimeTradePurchaseCommand();
var itemKey = purchase.ItemKey ?? "";
var catalog = context.Catalog;
var quantity = Math.Max(1, purchase.Quantity);
if (catalog == null)
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidTradeItem);
var typedItem = catalog.FindItem(itemKey);
if (typedItem == null)
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidTradeItem);
var actorKey = ResolveActorEntityKey(run, command);
if (!TryResolveDockParent(run, actorKey, out var dockParentKey, out var dockParent) ||
!IsEntityDockedAt(run, actorKey, dockParent))
{
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.TradeRequiresDocked);
}
if (!TryFindStationStock(
dockParent,
itemKey,
purchase.StationCargoIndex,
purchase.SourceX,
purchase.SourceY,
out var stationCargoIndex,
out var stockSlot))
{
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.TradeStockUnavailable);
}
var stationEntity = dockParent;
var unitPrice = AetheriaRuntimeDaemonTradeItemQueries.TradeItemValue(
typedItem,
stockSlot.Item,
catalog.TradeValueSettings).Price;
var totalPrice = checked(unitPrice * quantity);
if (unitPrice < 0 || run.Credits < totalPrice)
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.TradeInsufficientCredits);
var createsDockedShip = !string.IsNullOrWhiteSpace(typedItem.HullType);
if (createsDockedShip)
{
if (quantity != 1 || !HasAvailableDockingBay(dockParent))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.NoEligibleDockingBay);
if (!TryRemoveCargoItemQuantity(
stationEntity,
stationCargoIndex,
itemKey,
purchase.SourceX,
purchase.SourceY,
1,
out var purchasedHull))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.TradeStockUnavailable);
if (!ApplyCreateDockedShipPurchase(run, dockParentKey, itemKey, out var purchasedShipKey))
{
AddCargoItem(stationEntity, stationCargoIndex, purchasedHull);
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.NoEligibleDockingBay);
}
run.CurrentEntityKey = purchasedShipKey;
run.Credits -= totalPrice;
return true;
}
if (!TryResolveCargoBay(
run,
actorKey,
Math.Max(0, purchase.TargetCargoIndex),
out var targetEntity,
out var targetCargoIndex,
out _))
{
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.TradeTargetNoFit);
}
if (!AetheriaRuntimeRefitTransactions.TryTransferCargo(
stationEntity,
stationCargoIndex,
purchase.SourceX,
purchase.SourceY,
targetEntity,
targetCargoIndex,
itemKey,
quantity,
0,
0,
false,
catalog,
out var cargoReason))
{
return context.Reject(
string.Equals(cargoReason, AetheriaRuntimeDaemonRejectionReasons.InvalidCargoSource, StringComparison.Ordinal)
? AetheriaRuntimeDaemonRejectionReasons.TradeStockUnavailable
: AetheriaRuntimeDaemonRejectionReasons.TradeTargetNoFit);
}
run.Credits -= totalPrice;
return true;
}
private static bool ApplyTradeSale(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context)
{
var sale = command.TradeSale ?? new AetheriaRuntimeTradeSaleCommand();
var catalog = context.Catalog;
if (catalog == null || sale.Quantity <= 0 || string.IsNullOrWhiteSpace(sale.ItemKey))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidTradeSource);
if (!TryResolveEntity(run, run.CurrentEntityKey, out _, out _, out var seller) ||
!TryResolveDockParent(run, run.CurrentEntityKey, out _, out var station) ||
!IsEntityDockedAt(run, run.CurrentEntityKey, station))
{
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.TradeRequiresDocked);
}
var sellerCargo = seller.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
if (sale.SourceCargoIndex < 0 || sale.SourceCargoIndex >= sellerCargo.Count ||
!TryFindCargoItem(
sellerCargo[sale.SourceCargoIndex],
sale.ItemKey,
sale.SourceX,
sale.SourceY,
out var sourceSlot) ||
sourceSlot.Item == null ||
sale.Quantity > sourceSlot.Item.Quantity)
{
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidTradeSource);
}
var typedItem = catalog.FindItem(sale.ItemKey);
var unitPrice = AetheriaRuntimeDaemonTradeItemQueries.TradeItemValue(
typedItem,
sourceSlot.Item,
catalog.TradeValueSettings).Price;
var payout = (long)unitPrice * sale.Quantity;
if (typedItem == null || unitPrice < 0 || payout < 0 || payout > int.MaxValue - (long)run.Credits)
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidTradePayout);
var stationBayCount = Math.Min(
station.CargoBays?.Count ?? 0,
station.CargoContents?.Count ?? 0);
for (var stationCargoIndex = 0; stationCargoIndex < stationBayCount; stationCargoIndex++)
{
if (!AetheriaRuntimeRefitTransactions.TryTransferCargo(
seller,
sale.SourceCargoIndex,
sale.SourceX,
sale.SourceY,
station,
stationCargoIndex,
sale.ItemKey,
sale.Quantity,
0,
0,
false,
catalog,
out _))
{
continue;
}
run.Credits += (int)payout;
return true;
}
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.TradeStationNoFit);
}
private static bool TryResolveDockParent(
AetheriaRuntimeRunCheckpointCommit run,
string childEntityKey,
out string parentEntityKey,
out AetheriaRuntimeEntitySnapshotCommit parent)
{
parentEntityKey = "";
parent = null!;
if (!TryResolveEntity(run, childEntityKey, out var zoneIndex, out var childIndex, out _))
return false;
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == zoneIndex);
parent = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
.FirstOrDefault(entity => entity != null &&
(entity.DockingBayAssignments ?? Array.Empty<int>()).Contains(childIndex))!;
if (parent == null)
return false;
parentEntityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run.RunId, zoneIndex, parent.EntityIndex);
return true;
}
private static bool IsEntityDockedAt(
AetheriaRuntimeRunCheckpointCommit run,
string entityKey,
AetheriaRuntimeEntitySnapshotCommit parent)
{
return TryResolveEntity(run, entityKey, out _, out var entityIndex, out _) &&
(parent.DockingBayAssignments ?? Array.Empty<int>()).Contains(entityIndex);
}
private static bool HasAvailableDockingBay(AetheriaRuntimeEntitySnapshotCommit parent)
{
var bays = parent.DockingBays ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
var assignments = parent.DockingBayAssignments ?? Array.Empty<int>();
for (var index = 0; index < bays.Count; index++)
{
if (index >= assignments.Count || assignments[index] < 0)
return true;
}
return false;
}
private static bool TryFindCargoItem(
AetheriaRuntimeCargoBayLoadoutCommit cargo,
string itemKey,
int x,
int y,
out AetheriaRuntimeLoadoutItemSlotCommit slot)
{
slot = (cargo?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.FirstOrDefault(candidate => candidate?.Item != null &&
string.Equals(candidate.Item.ItemKey ?? "", itemKey, StringComparison.Ordinal) &&
candidate.X == x && candidate.Y == y)!;
return slot != null;
}
private static bool TryFindStationStock(
AetheriaRuntimeEntitySnapshotCommit station,
string itemKey,
int requestedCargoIndex,
int requestedX,
int requestedY,
out int cargoIndex,
out AetheriaRuntimeLoadoutItemSlotCommit slot)
{
cargoIndex = -1;
slot = null!;
var cargo = station.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
IEnumerable<int> indices = requestedCargoIndex >= 0 && requestedCargoIndex < cargo.Count
? new[] { requestedCargoIndex }
: Enumerable.Range(0, cargo.Count);
foreach (var index in indices)
{
if (TryFindCargoItem(cargo[index], itemKey, requestedX, requestedY, out slot))
{
cargoIndex = index;
return true;
}
}
return false;
}
private static bool ApplyCreateDockedShipPurchase(
AetheriaRuntimeRunCheckpointCommit run,
string dockParentKey,
string itemKey,
out string purchasedShipKey)
{
purchasedShipKey = "";
if (string.IsNullOrWhiteSpace(itemKey) ||
!TryResolveEntity(run, dockParentKey, out var zoneIndex, out var parentIndex, out var parent))
{
return false;
}
var zones = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>()).ToArray();
var zone = zones.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == zoneIndex);
if (zone == null)
return false;
var entities = (zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()).ToList();
var entityIndex = entities.Count;
var ship = new AetheriaRuntimeEntitySnapshotCommit
{
EntityIndex = entityIndex,
Name = string.IsNullOrWhiteSpace(itemKey) ? "Purchased Ship" : itemKey,
Kind = "ship",
HullItemKey = itemKey,
DirectionX = parent.DirectionX,
DirectionY = parent.DirectionY,
PositionX = parent.PositionX,
PositionY = parent.PositionY,
PositionZ = parent.PositionZ,
IsActive = true,
TargetEntityIndex = -1,
ShutdownPerformance = 0.25
};
entities.Add(ship);
zone.Entities = entities.ToArray();
run.Zones = zones;
var childIndices = (parent.ChildEntityIndices ?? Array.Empty<int>()).ToList();
if (!childIndices.Contains(entityIndex))
childIndices.Add(entityIndex);
parent.ChildEntityIndices = childIndices.ToArray();
var assignments = (parent.DockingBayAssignments ?? Array.Empty<int>()).ToList();
var assigned = false;
for (var index = 0; index < assignments.Count; index++)
{
if (assignments[index] >= 0)
continue;
assignments[index] = entityIndex;
assigned = true;
break;
}
if (!assigned)
assignments.Add(entityIndex);
parent.DockingBayAssignments = assignments.ToArray();
purchasedShipKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run.RunId, zoneIndex, entityIndex);
return true;
}
private static bool ApplyRestoreLoadout(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context)
{
var restore = command.LoadoutRestore ?? new AetheriaRuntimeLoadoutRestoreCommand();
var price = restore.Price;
if (price < 0 || run.Credits < price)
return false;
var templateName = restore.TemplateName ?? "";
if (string.IsNullOrWhiteSpace(command.TargetEntityKey) ||
string.IsNullOrWhiteSpace(templateName))
{
return false;
}
var template = (context.LoadoutTemplates ?? Array.Empty<AetheriaRuntimeLoadoutTemplateCommit>())
.FirstOrDefault(candidate =>
candidate != null &&
string.Equals(candidate.Name ?? "", templateName, StringComparison.Ordinal));
if (template == null)
return false;
if (!AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(command.TargetEntityKey, out var zoneIndex, out _))
return false;
var newEntityKey = run.AppendLoadoutTemplateToZone(
zoneIndex,
command.TargetEntityKey,
template);
if (string.IsNullOrWhiteSpace(newEntityKey))
return false;
run.CurrentEntityKey = newEntityKey;
run.Credits -= price;
return true;
}
private static bool ApplySetMoveVector(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonIntentState intents)
{
var actor = ResolveActorEntityKey(run, command);
if (!TryResolveEntity(run, actor, out _, out _, out _) ||
!IsNormalizedScalar(command.ScalarValue) ||
!IsFinite(command.DirectionX) ||
!IsFinite(command.DirectionY))
{
return false;
}
intents.Movements.RemoveAll(intent =>
string.Equals(intent.ActorEntityKey, actor, StringComparison.Ordinal));
intents.Movements.Add(new AetheriaRuntimeDaemonMovementIntent
{
ActorEntityKey = actor,
DirectionX = command.DirectionX,
DirectionY = command.DirectionY,
Magnitude = command.ScalarValue
});
return true;
}
private static bool ApplyFireWeaponGroup(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonIntentState intents)
{
var actor = ResolveActorEntityKey(run, command);
if (!TryResolveEntity(run, actor, out var zoneIndex, out var entityIndex, out var entity) ||
IsChildReferencedInZone(run, zoneIndex, entityIndex) ||
!HasWeaponGroup(entity, command.WeaponGroup))
{
return false;
}
intents.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
{
ActorEntityKey = actor,
WeaponGroup = command.WeaponGroup,
Fire = true,
Active = true
});
return true;
}
private static bool ApplySetWeaponGroupActive(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonIntentState intents)
{
var actor = ResolveActorEntityKey(run, command);
if (!TryResolveEntity(run, actor, out var zoneIndex, out var entityIndex, out var entity) ||
IsChildReferencedInZone(run, zoneIndex, entityIndex) ||
!HasWeaponGroup(entity, command.WeaponGroup))
{
return false;
}
var existingActiveGroups = entity.ActiveWeaponGroups ?? Array.Empty<bool>();
var activeGroups = Enumerable.Range(0, entity.WeaponGroups?.Count ?? 0)
.Select(index => index < existingActiveGroups.Count && existingActiveGroups[index])
.ToArray();
var active = command.ScalarValue > 0.5;
var pressed = active && !activeGroups[command.WeaponGroup];
activeGroups[command.WeaponGroup] = active;
entity.ActiveWeaponGroups = activeGroups;
intents.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
{
ActorEntityKey = actor,
WeaponGroup = command.WeaponGroup,
Fire = pressed,
Active = active
});
return true;
}
private static bool ApplySetBehaviorActive(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonIntentState intents)
{
var actor = ResolveActorEntityKey(run, command);
if (string.IsNullOrWhiteSpace(actor) ||
command.EquipmentIndex < 0 ||
command.BehaviorIndex < 0 ||
!TryResolveEntity(run, actor, out _, out _, out var entity) ||
!HasEquipmentBehavior(entity, command.EquipmentIndex, command.BehaviorIndex))
{
return false;
}
intents.Behaviors.Add(new AetheriaRuntimeDaemonBehaviorIntent
{
ActorEntityKey = actor,
EquipmentIndex = command.EquipmentIndex,
BehaviorIndex = command.BehaviorIndex,
Active = command.ScalarValue > 0.5,
TargetBodyKey = command.TextValue ?? "",
TargetAsteroidIndex = (int)command.PositionX
});
return true;
}
private static bool HasWeaponGroup(AetheriaRuntimeEntitySnapshotCommit entity, int weaponGroup)
{
var weaponGroups = entity.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>();
if (weaponGroup >= 0 && weaponGroup < weaponGroups.Count)
return true;
return weaponGroup == 0 &&
(entity.WeaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>())
.Any(state => state != null &&
string.Equals(state.BehaviorKind, "ProjectileWeapon", StringComparison.Ordinal));
}
private static bool HasEquipmentBehavior(
AetheriaRuntimeEntitySnapshotCommit entity,
int equipmentIndex,
int behaviorIndex)
{
return (entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
.Any(candidate =>
candidate != null &&
string.Equals(candidate.OwnerKind, "equipment", StringComparison.Ordinal) &&
candidate.OwnerIndex == equipmentIndex &&
candidate.BehaviorIndex == behaviorIndex);
}
private static bool IsNormalizedScalar(double value)
{
return IsFinite(value) && value >= 0.0 && value <= 1.0;
}
private static bool IsFinite(double value)
{
return !double.IsNaN(value) && !double.IsInfinity(value);
}
private static bool ApplyActivateConsumable(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonIntentState intents)
{
var actor = ResolveActorEntityKey(run, command);
if (string.IsNullOrWhiteSpace(actor) || string.IsNullOrWhiteSpace(command.TextValue))
return false;
intents.Consumables.Add(new AetheriaRuntimeDaemonConsumableIntent
{
ActorEntityKey = actor,
ItemKey = command.TextValue ?? ""
});
return true;
}
private static bool ApplySensorPing(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonIntentState intents)
{
var actor = ResolveActorEntityKey(run, command);
if (string.IsNullOrWhiteSpace(actor))
return false;
intents.SensorPings.Add(new AetheriaRuntimeDaemonSensorPingIntent
{
ActorEntityKey = actor
});
return true;
}
private static bool ApplyDockIntent(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context,
double maximumDistance)
{
var actor = ResolveActorEntityKey(run, command);
if (string.IsNullOrWhiteSpace(actor))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockActor);
if (string.IsNullOrWhiteSpace(command.TargetEntityKey))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockTarget);
if (!ApplyDockState(run, actor, command.TargetEntityKey, maximumDistance, context))
return false;
context.Intents.Docking.Add(new AetheriaRuntimeDaemonDockingIntent
{
ActorEntityKey = actor,
TargetEntityKey = command.TargetEntityKey ?? "",
Dock = true
});
AppendDockingEvent(run, command, "ship.docked", actor, command.TargetEntityKey ?? "");
return true;
}
private static bool ApplyDockNearestIntent(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context,
double defaultDockingDistance)
{
var actorKey = ResolveActorEntityKey(run, command);
if (string.IsNullOrWhiteSpace(actorKey) ||
!TryResolveEntity(run, actorKey, out _, out _, out _))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockActor);
// DockingDistance is daemon-owned gameplay state. A client-provided scalar must
// not widen or narrow the fossil's interaction rule.
var maximumDistance = defaultDockingDistance;
if (!TryFindFirstDockTarget(
run,
actorKey,
maximumDistance,
out var targetKey))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.NoEligibleDockingBay);
command.TargetEntityKey = targetKey;
return ApplyDockIntent(run, command, context, maximumDistance);
}
private static bool ApplyUndockIntent(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context)
{
var actor = ResolveActorEntityKey(run, command);
if (string.IsNullOrWhiteSpace(actor))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockActor);
var parentKey = TryResolveDockParent(run, actor, out var resolvedParentKey, out var resolvedParent)
? resolvedParentKey
: "";
var undockBayIndex = -1;
if (TryResolveEntity(run, actor, out _, out var resolvedActorIndex, out _) && resolvedParent != null)
{
var assignments = resolvedParent.DockingBayAssignments ?? Array.Empty<int>();
for (var index = 0; index < assignments.Count; index++)
if (assignments[index] == resolvedActorIndex)
{
undockBayIndex = index;
break;
}
}
if (!ApplyUndockState(run, actor, context))
return false;
context.Intents.Docking.Add(new AetheriaRuntimeDaemonDockingIntent
{
ActorEntityKey = actor,
Undock = true
});
AppendDockingEvent(run, command, "ship.undocked", actor, parentKey, undockBayIndex);
return true;
}
private static void AppendDockingEvent(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
string kind,
string actorEntityKey,
string parentEntityKey,
int knownBayIndex = -1)
{
if (!TryResolveEntity(run, actorEntityKey, out var zoneIndex, out var actorIndex, out var actor) ||
!TryResolveEntity(run, parentEntityKey, out _, out var parentIndex, out var parent))
return;
var bayIndex = knownBayIndex;
if (bayIndex < 0)
{
var assignments = parent.DockingBayAssignments ?? Array.Empty<int>();
for (var index = 0; index < assignments.Count; index++)
if (assignments[index] == actorIndex)
{
bayIndex = index;
break;
}
}
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
{
EventId = $"command:{command.CommandId}:{kind}",
Kind = kind,
FrameId = command.ObservedFrameId,
ZoneIndex = zoneIndex,
SourceEntityIndex = actorIndex,
TargetEntityIndex = parentIndex,
SubjectKey = actorEntityKey,
Reason = parentEntityKey,
ScalarValue = bayIndex,
PositionX = actor.PositionX,
PositionZ = actor.PositionZ
});
}
private static bool ApplyInteractIntent(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonOperationContext context,
double defaultDockingDistance,
double defaultWormholeExitRadius,
double wormholeDistanceRatio)
{
var actorKey = ResolveActorEntityKey(run, command);
if (string.IsNullOrWhiteSpace(actorKey) ||
!TryResolveEntity(run, actorKey, out var zoneIndex, out var actorIndex, out _))
{
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockActor);
}
if (IsChildReferencedInZone(run, zoneIndex, actorIndex))
return ApplyUndockIntent(run, command, context);
if (TryFindNearestWormholeTarget(
run,
actorKey,
ResolveInteractionDistance(command.PositionX, defaultWormholeExitRadius),
wormholeDistanceRatio,
out var targetZoneIndex,
out _,
out _))
{
command.TargetZoneIndex = targetZoneIndex;
return ApplyEnterWormholeIntent(run, command, context.Intents,
defaultWormholeExitRadius, wormholeDistanceRatio);
}
return ApplyDockNearestIntent(run, command, context, defaultDockingDistance);
}
private static double ResolveInteractionDistance(double commandDistance, double defaultDistance)
{
if (IsFinite(commandDistance) && commandDistance > 0.0)
return commandDistance;
return IsFinite(defaultDistance) && defaultDistance > 0.0
? defaultDistance
: double.PositiveInfinity;
}
private static bool TryFindFirstDockTarget(
AetheriaRuntimeRunCheckpointCommit run,
string actorEntityKey,
double maxDistance,
out string targetEntityKey)
{
targetEntityKey = "";
if (!TryResolveEntity(run, actorEntityKey, out var zoneIndex, out var actorIndex, out var actor))
return false;
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == zoneIndex);
if (zone == null)
return false;
var maxDistanceSq = maxDistance > 0.0 ? maxDistance * maxDistance : double.PositiveInfinity;
var entities = zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>();
for (var index = 0; index < entities.Count; index++)
{
var candidate = entities[index];
if (candidate == null || candidate.EntityIndex == actorIndex || !HasAvailableDockingBay(candidate))
continue;
var deltaX = candidate.PositionX - actor.PositionX;
var deltaZ = candidate.PositionZ - actor.PositionZ;
var distanceSq = (deltaX * deltaX) + (deltaZ * deltaZ);
if (distanceSq >= maxDistanceSq)
continue;
targetEntityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(
run.RunId, zoneIndex, candidate.EntityIndex);
return true;
}
return false;
}
private static bool TryFindNearestWormholeTarget(
AetheriaRuntimeRunCheckpointCommit run,
string actorEntityKey,
double maxDistance,
double wormholeDistanceRatio,
out int targetZoneIndex,
out double entryX,
out double entryY)
{
targetZoneIndex = -1;
entryX = 0.0;
entryY = 0.0;
if (!TryResolveEntity(run, actorEntityKey, out var zoneIndex, out _, out var actor))
return false;
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == zoneIndex);
if (zone == null)
return false;
var maxDistanceSq = maxDistance > 0.0 ? maxDistance * maxDistance : double.PositiveInfinity;
var closestDistanceSq = double.PositiveInfinity;
foreach (var exit in AetheriaRuntimeDaemonRenderQueries.QueryWormholeExits(
run,
zone,
AetheriaRuntimeDaemonRenderQueries.ResolveZoneRenderRadius(
zone, AetheriaRuntimeDaemonRenderQueries.DefaultZoneRenderRadius),
wormholeDistanceRatio))
{
var deltaX = exit.PositionX - actor.PositionX;
var deltaY = exit.PositionZ - actor.PositionZ;
var distanceSq = (deltaX * deltaX) + (deltaY * deltaY);
if (distanceSq >= maxDistanceSq || distanceSq >= closestDistanceSq)
continue;
closestDistanceSq = distanceSq;
targetZoneIndex = exit.TargetZoneIndex;
entryX = exit.PositionX;
entryY = exit.PositionZ;
}
return targetZoneIndex >= 0;
}
private static bool ApplyDockState(
AetheriaRuntimeRunCheckpointCommit run,
string actorEntityKey,
string targetEntityKey,
double maximumDistance,
AetheriaRuntimeDaemonOperationContext context)
{
if (!TryResolveEntity(run, actorEntityKey, out var actorZoneIndex, out var actorIndex, out var actor))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockActor);
if (
!TryResolveEntity(run, targetEntityKey, out var targetZoneIndex, out var targetIndex, out var target))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockTarget);
if (actorZoneIndex != targetZoneIndex || actorIndex == targetIndex)
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockTarget);
if (IsChildReferencedInZone(run, actorZoneIndex, actorIndex))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.AlreadyDocked);
var deltaX = target.PositionX - actor.PositionX;
var deltaZ = target.PositionZ - actor.PositionZ;
var maximumDistanceSq = maximumDistance > 0
? maximumDistance * maximumDistance
: double.PositiveInfinity;
if ((deltaX * deltaX) + (deltaZ * deltaZ) >= maximumDistanceSq)
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockTarget);
var bays = target.DockingBays ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>();
var existingAssignments = target.DockingBayAssignments ?? Array.Empty<int>();
var bayIndex = -1;
for (var index = 0; index < bays.Count; index++)
{
if (index >= existingAssignments.Count || existingAssignments[index] < 0)
{
bayIndex = index;
break;
}
}
if (bayIndex < 0)
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.NoEligibleDockingBay);
var childIndices = (target.ChildEntityIndices ?? Array.Empty<int>()).ToList();
if (!childIndices.Contains(actorIndex))
childIndices.Add(actorIndex);
target.ChildEntityIndices = childIndices.ToArray();
var assignments = Enumerable.Range(0, bays.Count)
.Select(index => index < existingAssignments.Count ? existingAssignments[index] : -1)
.ToArray();
assignments[bayIndex] = actorIndex;
target.DockingBayAssignments = assignments.ToArray();
DisarmWeapons(actor);
return true;
}
private static void DisarmWeapons(AetheriaRuntimeEntitySnapshotCommit entity)
{
entity.ActiveWeaponGroups = Array.Empty<bool>();
foreach (var state in entity.WeaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>())
{
if (state == null)
continue;
state.Firing = false;
state.TriggerPending = false;
state.BurstRemaining = 0;
state.Charging = false;
state.Charged = false;
state.Charge = 0;
state.ChargeHoldSeconds = 0;
state.ChargeRiskChecks = 0;
state.ChargeMalfunctionRisk = 0;
state.LockTargetEntityIndex = -1;
state.LockProgress = 0;
}
}
private static bool ApplyUndockState(
AetheriaRuntimeRunCheckpointCommit run,
string actorEntityKey,
AetheriaRuntimeDaemonOperationContext context)
{
if (!TryResolveEntity(run, actorEntityKey, out var zoneIndex, out var actorIndex, out var actor))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockActor);
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == zoneIndex);
var parent = (zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
.FirstOrDefault(entity => entity != null &&
((entity.ChildEntityIndices ?? Array.Empty<int>()).Contains(actorIndex) ||
(entity.DockingBayAssignments ?? Array.Empty<int>()).Contains(actorIndex)));
if (parent == null)
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.NotDocked);
var assignments = parent.DockingBayAssignments ?? Array.Empty<int>();
var dockingBayIndex = -1;
for (var index = 0; index < assignments.Count; index++)
{
if (assignments[index] == actorIndex)
{
dockingBayIndex = index;
break;
}
}
if (dockingBayIndex < 0)
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.NotDocked);
if (!HasInstalledBehavior(actor, context.Catalog, "Cockpit"))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.MissingCockpit);
if (!HasInstalledBehavior(actor, context.Catalog, "Thruster") &&
!HasInstalledBehavior(actor, context.Catalog, "AetherDrive"))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.MissingPropulsion);
if (!HasInstalledBehavior(actor, context.Catalog, "Reactor"))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.MissingReactor);
var dockingBayContents = parent.DockingBayContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
if (dockingBayIndex < dockingBayContents.Count &&
(dockingBayContents[dockingBayIndex]?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.Any(slot => slot?.Item != null))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.DockingBayCargoNotEmpty);
if (!RemoveChildReferenceFromZone(run, zoneIndex, actorIndex))
return context.Reject(AetheriaRuntimeDaemonRejectionReasons.NotDocked);
return true;
}
private static bool HasInstalledBehavior(
AetheriaRuntimeEntitySnapshotCommit actor,
AetheriaRuntimeCatalogSnapshot? catalog,
string behaviorKind) =>
AetheriaRuntimeEquippedBehaviorQueries.Find(actor, catalog, behaviorKind).Count > 0;
private static bool IsChildReferencedInZone(
AetheriaRuntimeRunCheckpointCommit run,
int zoneIndex,
int childEntityIndex)
{
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == zoneIndex);
if (zone == null)
return false;
foreach (var entity in zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
{
if (entity == null)
continue;
if ((entity.ChildEntityIndices ?? Array.Empty<int>()).Contains(childEntityIndex))
return true;
if ((entity.DockingBayAssignments ?? Array.Empty<int>()).Contains(childEntityIndex))
return true;
}
return false;
}
private static bool RemoveChildReferenceFromZone(
AetheriaRuntimeRunCheckpointCommit run,
int zoneIndex,
int childEntityIndex)
{
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == zoneIndex);
if (zone == null)
return false;
var removed = false;
foreach (var entity in zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
{
if (entity == null)
continue;
var childIndices = (entity.ChildEntityIndices ?? Array.Empty<int>()).ToList();
if (childIndices.RemoveAll(index => index == childEntityIndex) > 0)
{
entity.ChildEntityIndices = childIndices.ToArray();
removed = true;
}
var assignments = (entity.DockingBayAssignments ?? Array.Empty<int>()).ToArray();
for (var index = 0; index < assignments.Length; index++)
{
if (assignments[index] != childEntityIndex)
continue;
assignments[index] = -1;
removed = true;
}
entity.DockingBayAssignments = assignments;
}
return removed;
}
private static bool ApplyEnterWormholeIntent(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
AetheriaRuntimeDaemonIntentState intents,
double maximumDistance,
double wormholeDistanceRatio)
{
var actor = ResolveActorEntityKey(run, command);
if (string.IsNullOrWhiteSpace(actor) || command.TargetZoneIndex < 0 ||
!TryResolveEntity(run, actor, out var sourceZoneIndex, out var sourceEntityIndex, out var entity) ||
IsChildReferencedInZone(run, sourceZoneIndex, sourceEntityIndex) ||
intents.Wormholes.Any(intent => string.Equals(intent.ActorEntityKey, actor, StringComparison.Ordinal)) ||
entity.WormholeTransition != null)
return false;
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == sourceZoneIndex);
var exit = AetheriaRuntimeDaemonRenderQueries.QueryWormholeExits(
run,
zone,
AetheriaRuntimeDaemonRenderQueries.ResolveZoneRenderRadius(
zone, AetheriaRuntimeDaemonRenderQueries.DefaultZoneRenderRadius),
wormholeDistanceRatio)
.FirstOrDefault(candidate => candidate.TargetZoneIndex == command.TargetZoneIndex);
if (exit.TargetZoneIndex != command.TargetZoneIndex)
return false;
var dx = exit.PositionX - entity.PositionX;
var dz = exit.PositionZ - entity.PositionZ;
if (dx * dx + dz * dz >= maximumDistance * maximumDistance)
return false;
var targetZone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == command.TargetZoneIndex);
var returnExit = AetheriaRuntimeDaemonRenderQueries.QueryWormholeExits(
run,
targetZone,
AetheriaRuntimeDaemonRenderQueries.ResolveZoneRenderRadius(
targetZone, AetheriaRuntimeDaemonRenderQueries.DefaultZoneRenderRadius),
wormholeDistanceRatio)
.FirstOrDefault(candidate => candidate.TargetZoneIndex == sourceZoneIndex);
if (returnExit.TargetZoneIndex != sourceZoneIndex)
return false;
intents.Wormholes.Add(new AetheriaRuntimeDaemonWormholeIntent
{
ActorEntityKey = actor,
SourceZoneIndex = sourceZoneIndex,
TargetZoneIndex = command.TargetZoneIndex,
EntryWormholeX = exit.PositionX,
EntryWormholeZ = exit.PositionZ,
ExitWormholeX = returnExit.PositionX,
ExitWormholeZ = returnExit.PositionZ,
CommandId = command.CommandId ?? ""
});
return true;
}
private static bool ApplyTowToStation(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command,
double attachmentDistance)
{
var actor = ResolveActorEntityKey(run, command);
if (!TryResolveEntity(run, actor, out var actorZone, out _, out var towing) ||
!TryResolveEntity(run, command.TargetEntityKey, out var stationZone, out var stationIndex, out var station) || actorZone != stationZone)
return false;
var children = (towing.ChildEntityIndices ?? Array.Empty<int>()).ToList();
if (string.Equals(command.TextValue, "attach", StringComparison.Ordinal))
{
if (children.Contains(stationIndex) || Math.Pow(station.PositionX - towing.PositionX, 2) + Math.Pow(station.PositionZ - towing.PositionZ, 2) > attachmentDistance * attachmentDistance)
return false;
children.Add(stationIndex); towing.ChildEntityIndices = children; station.OrbitKey = ""; station.PositionX = towing.PositionX; station.PositionZ = towing.PositionZ; return true;
}
if (!string.Equals(command.TextValue, "detach", StringComparison.Ordinal) || !children.Remove(stationIndex) || string.IsNullOrWhiteSpace(command.SubjectKey) || command.ScalarValue <= 0)
return false;
towing.ChildEntityIndices = children; station.PositionX = command.PositionX; station.PositionZ = command.PositionZ;
var zone = run.Zones.First(value => value.ZoneIndex == actorZone);
var orbitKey = $"tow:{command.CommandId}";
zone.Orbits = (zone.Orbits ?? Array.Empty<AetheriaRuntimeOrbitSnapshotCommit>()).Concat(new[] { new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = orbitKey, ParentOrbitKey = command.SubjectKey, Distance = command.ScalarValue, FixedPositionX = command.PositionX, FixedPositionY = command.PositionZ } }).ToArray();
station.OrbitKey = orbitKey; return true;
}
internal static bool MoveEntityToZone(
AetheriaRuntimeRunCheckpointCommit run,
string entityKey,
int targetZoneIndex,
double positionX,
double positionY,
out string movedEntityKey)
{
movedEntityKey = "";
if (!AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(entityKey, out var sourceZoneIndex, out var sourceEntityIndex))
return false;
var zones = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>()).ToList();
var sourceZone = zones.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == sourceZoneIndex);
if (sourceZone == null)
return false;
var sourceEntities = (sourceZone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()).ToList();
if (sourceEntityIndex < 0 || sourceEntityIndex >= sourceEntities.Count)
return false;
var movedEntity = sourceEntities[sourceEntityIndex];
if (movedEntity == null)
return false;
if (IsChildReferencedInZone(run, sourceZoneIndex, sourceEntityIndex))
return false;
var movedCurrentEntity = AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(run.CurrentEntityKey, out var currentZoneIndex, out var currentEntityIndex) &&
currentZoneIndex == sourceZoneIndex &&
currentEntityIndex == sourceEntityIndex;
RemoveChildReferenceFromZone(run, sourceZoneIndex, sourceEntityIndex);
sourceEntities.RemoveAt(sourceEntityIndex);
sourceZone.Entities = sourceEntities.ToArray();
ReindexZoneAfterEntityRemoval(sourceZone, sourceEntityIndex);
var targetZone = zones.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == targetZoneIndex);
if (targetZone == null)
{
targetZone = new AetheriaRuntimeZoneSnapshotCommit
{
ZoneIndex = targetZoneIndex,
Entities = Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()
};
zones.Add(targetZone);
run.Zones = zones.ToArray();
}
var targetEntities = (targetZone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()).ToList();
var targetEntityIndex = targetEntities.Count;
movedEntity.EntityIndex = targetEntityIndex;
movedEntity.TargetEntityIndex = -1;
movedEntity.PositionX = positionX;
movedEntity.PositionZ = positionY;
targetEntities.Add(movedEntity);
targetZone.Entities = targetEntities.ToArray();
movedEntityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run.RunId, targetZoneIndex, targetEntityIndex);
if (movedCurrentEntity)
{
run.CurrentZoneIndex = targetZoneIndex;
run.CurrentEntityKey = movedEntityKey;
}
else
{
ReindexCurrentEntityKeyAfterRemoval(run, sourceZoneIndex, sourceEntityIndex);
}
var discovered = (run.DiscoveredZoneIndices ?? Array.Empty<int>()).ToList();
if (!discovered.Contains(targetZoneIndex))
{
discovered.Add(targetZoneIndex);
run.DiscoveredZoneIndices = discovered.ToArray();
}
return true;
}
private static void ReindexZoneAfterEntityRemoval(AetheriaRuntimeZoneSnapshotCommit zone, int removedEntityIndex)
{
var entities = zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>();
for (var index = 0; index < entities.Count; index++)
{
var entity = entities[index];
if (entity == null)
continue;
entity.EntityIndex = index;
entity.TargetEntityIndex = ReindexEntityReference(entity.TargetEntityIndex, removedEntityIndex);
entity.ChildEntityIndices = (entity.ChildEntityIndices ?? Array.Empty<int>())
.Select(childIndex => ReindexEntityReference(childIndex, removedEntityIndex))
.Where(childIndex => childIndex >= 0)
.ToArray();
entity.DockingBayAssignments = (entity.DockingBayAssignments ?? Array.Empty<int>())
.Select(childIndex => ReindexEntityReference(childIndex, removedEntityIndex))
.ToArray();
entity.Contacts = (entity.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
.Select(contact =>
{
if (contact != null)
contact.TargetEntityIndex = ReindexEntityReference(contact.TargetEntityIndex, removedEntityIndex);
return contact;
})
.Where(contact => contact != null && contact.TargetEntityIndex >= 0)
.ToArray();
}
}
private static void ReindexCurrentEntityKeyAfterRemoval(
AetheriaRuntimeRunCheckpointCommit run,
int zoneIndex,
int removedEntityIndex)
{
if (!AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(run.CurrentEntityKey, out var currentZoneIndex, out var currentEntityIndex) ||
currentZoneIndex != zoneIndex)
{
return;
}
var reindexedCurrent = ReindexEntityReference(currentEntityIndex, removedEntityIndex);
run.CurrentEntityKey = reindexedCurrent < 0
? ""
: AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run.RunId, zoneIndex, reindexedCurrent);
}
private static bool ApplyTargetEntity(
AetheriaRuntimeRunCheckpointCommit run,
string entityKey,
Action<AetheriaRuntimeEntitySnapshotCommit> mutate)
{
if (!TryResolveEntity(run, entityKey, out _, out _, out var entity))
return false;
mutate(entity);
return true;
}
private static bool TryResolveEntity(
AetheriaRuntimeRunCheckpointCommit run,
string entityKey,
out int zoneIndex,
out int entityIndex,
out AetheriaRuntimeEntitySnapshotCommit entity)
{
entity = null!;
zoneIndex = -1;
entityIndex = -1;
if (!AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(entityKey, out zoneIndex, out entityIndex))
return false;
var parsedZoneIndex = zoneIndex;
var zone = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == parsedZoneIndex);
var entities = zone?.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>();
if (entityIndex < 0 || entityIndex >= entities.Count)
return false;
entity = entities[entityIndex];
return entity != null;
}
private static int ReindexEntityReference(int referencedEntityIndex, int removedEntityIndex)
{
if (referencedEntityIndex < 0)
return referencedEntityIndex;
if (referencedEntityIndex == removedEntityIndex)
return -1;
return referencedEntityIndex > removedEntityIndex
? referencedEntityIndex - 1
: referencedEntityIndex;
}
private static string ResolveActorEntityKey(
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeDaemonCommandDocument command)
{
return string.IsNullOrWhiteSpace(command.ActorEntityKey)
? run.CurrentEntityKey ?? ""
: command.ActorEntityKey ?? "";
}
private static bool TryResolveCargoBay(
AetheriaRuntimeRunCheckpointCommit run,
string entityKey,
int cargoIndex,
out AetheriaRuntimeEntitySnapshotCommit entity,
out int resolvedCargoIndex,
out AetheriaRuntimeCargoBayLoadoutCommit cargo)
{
entity = null!;
cargo = null!;
resolvedCargoIndex = -1;
if (!TryResolveEntity(run, entityKey, out _, out _, out entity))
return false;
var cargoContents = entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
if (cargoIndex < 0 || cargoIndex >= cargoContents.Count)
return false;
cargo = cargoContents[cargoIndex];
if (cargo == null)
return false;
resolvedCargoIndex = cargoIndex;
return true;
}
private static bool TryRemoveCargoItem(
AetheriaRuntimeEntitySnapshotCommit entity,
int cargoIndex,
string itemKey,
int x,
int y,
out AetheriaRuntimeLoadoutItemSlotCommit slot)
{
slot = null!;
var cargoContents = (entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).ToArray();
if (cargoIndex < 0 || cargoIndex >= cargoContents.Length || cargoContents[cargoIndex] == null)
return false;
var items = (cargoContents[cargoIndex].Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).ToList();
var itemIndex = items.FindIndex(candidate => IsCargoSlotMatch(candidate, itemKey, x, y));
if (itemIndex < 0)
return false;
slot = items[itemIndex];
items.RemoveAt(itemIndex);
cargoContents[cargoIndex] = new AetheriaRuntimeCargoBayLoadoutCommit
{
Items = items.ToArray()
};
entity.CargoContents = cargoContents;
return true;
}
private static bool TryRemoveCargoItemQuantity(
AetheriaRuntimeEntitySnapshotCommit entity,
int cargoIndex,
string itemKey,
int x,
int y,
int quantity,
out AetheriaRuntimeLoadoutItemSlotCommit slot)
{
return AetheriaRuntimeCargoTransactions.TryRemoveQuantity(
entity, cargoIndex, itemKey, x, y, quantity, out slot);
}
private static void AddCargoItem(
AetheriaRuntimeEntitySnapshotCommit entity,
int cargoIndex,
AetheriaRuntimeLoadoutItemSlotCommit slot)
{
var cargoContents = EnsureCargoContents(entity, cargoIndex + 1);
var cargo = cargoContents[cargoIndex] ?? new AetheriaRuntimeCargoBayLoadoutCommit();
cargoContents[cargoIndex] = new AetheriaRuntimeCargoBayLoadoutCommit
{
Items = (cargo.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.Concat(new[] { slot })
.ToArray()
};
entity.CargoContents = cargoContents;
}
private static AetheriaRuntimeLoadoutItemSlotCommit CloneSlot(AetheriaRuntimeLoadoutItemSlotCommit slot)
{
var item = slot?.Item;
return new AetheriaRuntimeLoadoutItemSlotCommit
{
X = slot?.X ?? 0,
Y = slot?.Y ?? 0,
Rotation = slot?.Rotation ?? "None",
Item = new AetheriaRuntimeLoadoutItemCommit
{
ItemKey = item?.ItemKey ?? "",
Quality = item?.Quality ?? 1.0,
Durability = item?.Durability ?? 1.0,
Quantity = item?.Quantity ?? 1,
Enabled = item?.Enabled ?? true,
OverrideShutdown = item?.OverrideShutdown ?? false,
Temperature = item?.Temperature ?? 0
}
};
}
private static bool TryRemoveEquipmentItem(
AetheriaRuntimeRunCheckpointCommit run,
string entityKey,
int equipmentIndex,
string itemKey,
out AetheriaRuntimeLoadoutItemSlotCommit slot)
{
slot = null!;
if (!TryResolveEntity(run, entityKey, out _, out _, out var entity))
return false;
var equipment = (entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).ToList();
if (equipmentIndex < 0 || equipmentIndex >= equipment.Count)
return false;
slot = equipment[equipmentIndex];
if (!IsItemMatch(slot?.Item, itemKey))
return false;
equipment.RemoveAt(equipmentIndex);
entity.Equipment = equipment.ToArray();
entity.WeaponGroups = (entity.WeaponGroups ?? Array.Empty<IReadOnlyList<int>>())
.Select(group => (IReadOnlyList<int>)(group ?? Array.Empty<int>())
.Where(index => index != equipmentIndex)
.Select(index => index > equipmentIndex ? index - 1 : index)
.ToArray())
.ToArray();
return true;
}
private static void AddEquipmentItem(
AetheriaRuntimeEntitySnapshotCommit entity,
AetheriaRuntimeLoadoutItemSlotCommit slot)
{
entity.Equipment = (entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
.Concat(new[] { slot })
.ToArray();
}
private static AetheriaRuntimeCargoBayLoadoutCommit[] EnsureCargoContents(
AetheriaRuntimeEntitySnapshotCommit entity,
int count)
{
var source = entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
var cargoContents = new AetheriaRuntimeCargoBayLoadoutCommit[Math.Max(source.Count, count)];
for (var i = 0; i < cargoContents.Length; i++)
{
cargoContents[i] = i < source.Count
? source[i] ?? new AetheriaRuntimeCargoBayLoadoutCommit()
: new AetheriaRuntimeCargoBayLoadoutCommit();
}
return cargoContents;
}
private static bool IsCargoSlotMatch(
AetheriaRuntimeLoadoutItemSlotCommit slot,
string itemKey,
int x,
int y)
{
if (slot == null || !IsItemMatch(slot.Item, itemKey))
return false;
return x == int.MinValue ||
y == int.MinValue ||
(slot.X == x && slot.Y == y);
}
private static bool IsItemMatch(AetheriaRuntimeLoadoutItemCommit item, string itemKey)
{
return item != null &&
(string.IsNullOrWhiteSpace(itemKey) ||
string.Equals(item.ItemKey ?? "", itemKey ?? "", StringComparison.Ordinal));
}
}
}