From 706e1cda8f3c18b4e196f20685283bab97aecad2 Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Fri, 3 Apr 2026 01:12:26 -0400 Subject: [PATCH] Refactor runtime bootstrap and ship control flows --- .../backend/Auth/Api/ForgotPasswordHandler.cs | 17 + apps/backend/Auth/Api/LoginHandler.cs | 25 + apps/backend/Auth/Api/RefreshTokenHandler.cs | 25 + apps/backend/Auth/Api/RegisterHandler.cs | 25 + apps/backend/Auth/Api/ResetPasswordHandler.cs | 26 + apps/backend/Auth/Contracts/AuthContracts.cs | 42 + .../backend/Auth/Runtime/AuthRuntimeModels.cs | 24 + apps/backend/Auth/Simulation/AuthOptions.cs | 14 + .../Auth/Simulation/AuthPolicyNames.cs | 13 + .../Auth/Simulation/AuthSchemaInitializer.cs | 41 + apps/backend/Auth/Simulation/AuthService.cs | 120 + apps/backend/Auth/Simulation/DevAuthSeeder.cs | 33 + .../Simulation/DevPasswordResetDelivery.cs | 7 + .../HttpContextPlayerIdentityResolver.cs | 23 + .../Auth/Simulation/IAuthRepository.cs | 17 + .../Auth/Simulation/IPasswordResetDelivery.cs | 6 + .../Simulation/IPlayerIdentityResolver.cs | 8 + apps/backend/Auth/Simulation/ITokenService.cs | 7 + apps/backend/Auth/Simulation/JwtOptions.cs | 10 + .../Auth/Simulation/JwtTokenService.cs | 51 + .../Auth/Simulation/LocalPasswordHasher.cs | 42 + .../Auth/Simulation/PostgresAuthRepository.cs | 199 ++ .../Auth/Simulation/RefreshTokenFactory.cs | 21 + apps/backend/Definitions/WorldDefinitions.cs | 129 +- .../Factions/AI/CommanderPlanningService.cs | 177 +- .../GeopoliticalSimulationService.cs | 28 +- apps/backend/GlobalUsings.cs | 5 +- .../Api/CreatePlayerOrganizationHandler.cs | 1 - .../Api/DeletePlayerDirectiveHandler.cs | 1 - .../Api/DeletePlayerOrganizationHandler.cs | 1 - .../Api/GetPlayerFactionHandler.cs | 1 - ...datePlayerOrganizationMembershipHandler.cs | 1 - .../Api/UpdatePlayerStrategicIntentHandler.cs | 1 - .../Api/UpsertPlayerAssignmentHandler.cs | 1 - .../UpsertPlayerAutomationPolicyHandler.cs | 1 - .../Api/UpsertPlayerDirectiveHandler.cs | 1 - .../Api/UpsertPlayerPolicyHandler.cs | 1 - .../UpsertPlayerProductionProgramHandler.cs | 1 - .../UpsertPlayerReinforcementPolicyHandler.cs | 1 - .../Runtime/PlayerFactionRuntimeModels.cs | 6 +- .../Simulation/IPlayerStateStore.cs | 9 + .../PlayerFactionProjectionService.cs | 270 ++ .../Simulation/PlayerFactionService.cs | 162 +- .../Simulation/PlayerStateStore.cs | 26 + apps/backend/Program.cs | 77 +- apps/backend/Shared/Contracts/VersionInfo.cs | 7 + .../Shared/Runtime/AppVersionService.cs | 29 + .../Shared/Runtime/KnownShipTaxonomy.cs | 69 + .../Shared/Runtime/ShipAutomationCatalog.cs | 121 + .../backend/Shared/Runtime/SimulationKinds.cs | 20 + .../Runtime/SimulationRuntimeSupport.cs | 58 +- .../Ships/AI/ShipAiService.BehaviorQueue.cs | 784 +++++ apps/backend/Ships/AI/ShipAiService.Data.cs | 54 + .../Ships/AI/ShipAiService.Execution.cs | 770 +++++ .../backend/Ships/AI/ShipAiService.Helpers.cs | 947 ++++++ .../AI/ShipAiService.Planning.Behaviors.cs | 319 ++ .../Ships/AI/ShipAiService.Planning.Orders.cs | 461 +++ apps/backend/Ships/AI/ShipAiService.cs | 216 ++ apps/backend/Ships/AI/ShipBootstrapPolicy.cs | 31 + .../Ships/Api/EnqueueShipOrderHandler.cs | 1 - .../Api/GetShipAutomationCatalogHandler.cs | 35 + .../Ships/Api/RemoveShipOrderHandler.cs | 1 - .../Api/UpdateShipDefaultBehaviorHandler.cs | 1 - .../backend/Ships/Contracts/ShipAutomation.cs | 19 + apps/backend/Ships/Contracts/ShipCommands.cs | 2 +- apps/backend/Ships/Contracts/Ships.cs | 16 +- .../Ships/Runtime/ShipRuntimeModels.cs | 4 +- .../backend/Ships/Simulation/ShipAiService.cs | 2705 ----------------- .../Ships/Simulation/ShipBootstrapPolicy.cs | 16 - .../Simulation/Core/SimulationEngine.cs | 12 +- .../Core/SimulationProjectionService.cs | 358 +-- apps/backend/SpaceGame.Api.csproj | 2 + .../Simulation/StationLifecycleService.cs | 16 +- .../Simulation/StationSimulationService.cs | 58 +- .../Universe/Api/CreateFactionHandler.cs | 25 + .../backend/Universe/Api/GetBalanceHandler.cs | 2 +- .../Universe/Api/GetTelemetryHandler.cs | 2 +- .../backend/Universe/Api/GetVersionHandler.cs | 17 + .../backend/Universe/Api/ResetWorldHandler.cs | 2 +- apps/backend/Universe/Api/SpawnShipHandler.cs | 25 + .../Universe/Api/SpawnStationHandler.cs | 25 + .../Universe/Api/UpdateBalanceHandler.cs | 2 +- .../Universe/Bootstrap/StaticDataProvider.cs | 19 +- apps/backend/Universe/Contracts/GmCommands.cs | 16 + apps/backend/Universe/Contracts/World.cs | 2 - .../Universe/Scenario/LoaderSupport.cs | 3 - .../Scenario/ScenarioContentBuilder.cs | 27 +- .../Scenario/SystemGenerationService.cs | 2 + .../Scenario/WorldRuntimeAssembler.cs | 4 - .../Universe/Scenario/WorldSeedingService.cs | 5 +- .../Universe/Simulation/WorldService.cs | 596 +++- apps/backend/appsettings.Development.json | 28 +- apps/backend/appsettings.json | 14 +- apps/viewer/src/App.vue | 104 +- apps/viewer/src/ViewerAppController.ts | 12 + apps/viewer/src/api.ts | 119 +- apps/viewer/src/assets/backdrop1.webp | Bin 0 -> 348810 bytes apps/viewer/src/authSession.ts | 76 + .../viewer/src/components/AuthLandingPage.vue | 185 ++ .../src/components/AuthSessionPanel.vue | 138 + .../components/ViewerEntityBrowserPanel.vue | 322 ++ .../components/ViewerEntityInspectorPanel.vue | 594 ++++ .../components/ViewerShipOrderContextMenu.vue | 320 ++ apps/viewer/src/components/gm/GmOpsWindow.vue | 200 +- .../components/gm/GmPlayerFactionPanel.vue | 105 +- apps/viewer/src/contractsAuth.ts | 14 + apps/viewer/src/contractsShipAutomation.ts | 20 + apps/viewer/src/contractsShips.ts | 10 +- apps/viewer/src/contractsWorld.ts | 3 - apps/viewer/src/main.ts | 3 + apps/viewer/src/shipAutomationPresentation.ts | 59 + apps/viewer/src/shipCommands.ts | 2 +- apps/viewer/src/styles/viewer.css | 729 +++++ apps/viewer/src/ui/stores/authStore.ts | 41 + apps/viewer/src/ui/stores/gmStore.ts | 20 + .../ui/stores/shipAutomationCatalogStore.ts | 48 + .../src/ui/stores/viewerOrderContextMenu.ts | 32 + apps/viewer/src/ui/stores/viewerScene.ts | 19 + apps/viewer/src/viewerControllerFactory.ts | 10 + apps/viewer/src/viewerHistory.ts | 2 +- .../viewer/src/viewerInteractionController.ts | 86 + apps/viewer/src/viewerOpsStrip.ts | 15 +- apps/viewer/src/viewerPanels.ts | 19 +- apps/viewer/src/viewerSceneAppearance.ts | 21 +- apps/viewer/src/viewerSelection.ts | 13 +- apps/viewer/src/viewerState.ts | 8 +- apps/viewer/src/viewerTypes.ts | 2 - apps/viewer/src/viewerWorldLifecycle.ts | 3 +- worksheet.md | 137 + 129 files changed, 9588 insertions(+), 3548 deletions(-) create mode 100644 apps/backend/Auth/Api/ForgotPasswordHandler.cs create mode 100644 apps/backend/Auth/Api/LoginHandler.cs create mode 100644 apps/backend/Auth/Api/RefreshTokenHandler.cs create mode 100644 apps/backend/Auth/Api/RegisterHandler.cs create mode 100644 apps/backend/Auth/Api/ResetPasswordHandler.cs create mode 100644 apps/backend/Auth/Contracts/AuthContracts.cs create mode 100644 apps/backend/Auth/Runtime/AuthRuntimeModels.cs create mode 100644 apps/backend/Auth/Simulation/AuthOptions.cs create mode 100644 apps/backend/Auth/Simulation/AuthPolicyNames.cs create mode 100644 apps/backend/Auth/Simulation/AuthSchemaInitializer.cs create mode 100644 apps/backend/Auth/Simulation/AuthService.cs create mode 100644 apps/backend/Auth/Simulation/DevAuthSeeder.cs create mode 100644 apps/backend/Auth/Simulation/DevPasswordResetDelivery.cs create mode 100644 apps/backend/Auth/Simulation/HttpContextPlayerIdentityResolver.cs create mode 100644 apps/backend/Auth/Simulation/IAuthRepository.cs create mode 100644 apps/backend/Auth/Simulation/IPasswordResetDelivery.cs create mode 100644 apps/backend/Auth/Simulation/IPlayerIdentityResolver.cs create mode 100644 apps/backend/Auth/Simulation/ITokenService.cs create mode 100644 apps/backend/Auth/Simulation/JwtOptions.cs create mode 100644 apps/backend/Auth/Simulation/JwtTokenService.cs create mode 100644 apps/backend/Auth/Simulation/LocalPasswordHasher.cs create mode 100644 apps/backend/Auth/Simulation/PostgresAuthRepository.cs create mode 100644 apps/backend/Auth/Simulation/RefreshTokenFactory.cs create mode 100644 apps/backend/PlayerFaction/Simulation/IPlayerStateStore.cs create mode 100644 apps/backend/PlayerFaction/Simulation/PlayerFactionProjectionService.cs create mode 100644 apps/backend/PlayerFaction/Simulation/PlayerStateStore.cs create mode 100644 apps/backend/Shared/Contracts/VersionInfo.cs create mode 100644 apps/backend/Shared/Runtime/AppVersionService.cs create mode 100644 apps/backend/Shared/Runtime/KnownShipTaxonomy.cs create mode 100644 apps/backend/Shared/Runtime/ShipAutomationCatalog.cs create mode 100644 apps/backend/Ships/AI/ShipAiService.BehaviorQueue.cs create mode 100644 apps/backend/Ships/AI/ShipAiService.Data.cs create mode 100644 apps/backend/Ships/AI/ShipAiService.Execution.cs create mode 100644 apps/backend/Ships/AI/ShipAiService.Helpers.cs create mode 100644 apps/backend/Ships/AI/ShipAiService.Planning.Behaviors.cs create mode 100644 apps/backend/Ships/AI/ShipAiService.Planning.Orders.cs create mode 100644 apps/backend/Ships/AI/ShipAiService.cs create mode 100644 apps/backend/Ships/AI/ShipBootstrapPolicy.cs create mode 100644 apps/backend/Ships/Api/GetShipAutomationCatalogHandler.cs create mode 100644 apps/backend/Ships/Contracts/ShipAutomation.cs delete mode 100644 apps/backend/Ships/Simulation/ShipAiService.cs delete mode 100644 apps/backend/Ships/Simulation/ShipBootstrapPolicy.cs create mode 100644 apps/backend/Universe/Api/CreateFactionHandler.cs create mode 100644 apps/backend/Universe/Api/GetVersionHandler.cs create mode 100644 apps/backend/Universe/Api/SpawnShipHandler.cs create mode 100644 apps/backend/Universe/Api/SpawnStationHandler.cs create mode 100644 apps/backend/Universe/Contracts/GmCommands.cs create mode 100644 apps/viewer/src/assets/backdrop1.webp create mode 100644 apps/viewer/src/authSession.ts create mode 100644 apps/viewer/src/components/AuthLandingPage.vue create mode 100644 apps/viewer/src/components/AuthSessionPanel.vue create mode 100644 apps/viewer/src/components/ViewerEntityBrowserPanel.vue create mode 100644 apps/viewer/src/components/ViewerEntityInspectorPanel.vue create mode 100644 apps/viewer/src/components/ViewerShipOrderContextMenu.vue create mode 100644 apps/viewer/src/contractsAuth.ts create mode 100644 apps/viewer/src/contractsShipAutomation.ts create mode 100644 apps/viewer/src/shipAutomationPresentation.ts create mode 100644 apps/viewer/src/ui/stores/authStore.ts create mode 100644 apps/viewer/src/ui/stores/shipAutomationCatalogStore.ts create mode 100644 apps/viewer/src/ui/stores/viewerOrderContextMenu.ts create mode 100644 apps/viewer/src/ui/stores/viewerScene.ts create mode 100644 worksheet.md diff --git a/apps/backend/Auth/Api/ForgotPasswordHandler.cs b/apps/backend/Auth/Api/ForgotPasswordHandler.cs new file mode 100644 index 0000000..dd80054 --- /dev/null +++ b/apps/backend/Auth/Api/ForgotPasswordHandler.cs @@ -0,0 +1,17 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Auth.Api; + +public sealed class ForgotPasswordHandler(AuthService authService) : Endpoint +{ + public override void Configure() + { + Post("/api/auth/forgot-password"); + AllowAnonymous(); + } + + public override async Task HandleAsync(ForgotPasswordRequest request, CancellationToken cancellationToken) + { + await SendOkAsync(await authService.ForgotPasswordAsync(request, cancellationToken), cancellationToken); + } +} diff --git a/apps/backend/Auth/Api/LoginHandler.cs b/apps/backend/Auth/Api/LoginHandler.cs new file mode 100644 index 0000000..c58988b --- /dev/null +++ b/apps/backend/Auth/Api/LoginHandler.cs @@ -0,0 +1,25 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Auth.Api; + +public sealed class LoginHandler(AuthService authService) : Endpoint +{ + public override void Configure() + { + Post("/api/auth/login"); + AllowAnonymous(); + } + + public override async Task HandleAsync(LoginRequest request, CancellationToken cancellationToken) + { + try + { + await SendOkAsync(await authService.LoginAsync(request, cancellationToken), cancellationToken); + } + catch (InvalidOperationException ex) + { + AddError(ex.Message); + await SendErrorsAsync(cancellation: cancellationToken); + } + } +} diff --git a/apps/backend/Auth/Api/RefreshTokenHandler.cs b/apps/backend/Auth/Api/RefreshTokenHandler.cs new file mode 100644 index 0000000..21dcdc1 --- /dev/null +++ b/apps/backend/Auth/Api/RefreshTokenHandler.cs @@ -0,0 +1,25 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Auth.Api; + +public sealed class RefreshTokenHandler(AuthService authService) : Endpoint +{ + public override void Configure() + { + Post("/api/auth/refresh"); + AllowAnonymous(); + } + + public override async Task HandleAsync(RefreshTokenRequest request, CancellationToken cancellationToken) + { + try + { + await SendOkAsync(await authService.RefreshAsync(request, cancellationToken), cancellationToken); + } + catch (InvalidOperationException ex) + { + AddError(ex.Message); + await SendErrorsAsync(cancellation: cancellationToken); + } + } +} diff --git a/apps/backend/Auth/Api/RegisterHandler.cs b/apps/backend/Auth/Api/RegisterHandler.cs new file mode 100644 index 0000000..e7cc370 --- /dev/null +++ b/apps/backend/Auth/Api/RegisterHandler.cs @@ -0,0 +1,25 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Auth.Api; + +public sealed class RegisterHandler(AuthService authService) : Endpoint +{ + public override void Configure() + { + Post("/api/auth/register"); + AllowAnonymous(); + } + + public override async Task HandleAsync(RegisterRequest request, CancellationToken cancellationToken) + { + try + { + await SendOkAsync(await authService.RegisterAsync(request, cancellationToken), cancellationToken); + } + catch (InvalidOperationException ex) + { + AddError(ex.Message); + await SendErrorsAsync(cancellation: cancellationToken); + } + } +} diff --git a/apps/backend/Auth/Api/ResetPasswordHandler.cs b/apps/backend/Auth/Api/ResetPasswordHandler.cs new file mode 100644 index 0000000..53fd201 --- /dev/null +++ b/apps/backend/Auth/Api/ResetPasswordHandler.cs @@ -0,0 +1,26 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Auth.Api; + +public sealed class ResetPasswordHandler(AuthService authService) : Endpoint +{ + public override void Configure() + { + Post("/api/auth/reset-password"); + AllowAnonymous(); + } + + public override async Task HandleAsync(ResetPasswordRequest request, CancellationToken cancellationToken) + { + try + { + await authService.ResetPasswordAsync(request, cancellationToken); + await SendNoContentAsync(cancellationToken); + } + catch (InvalidOperationException ex) + { + AddError(ex.Message); + await SendErrorsAsync(cancellation: cancellationToken); + } + } +} diff --git a/apps/backend/Auth/Contracts/AuthContracts.cs b/apps/backend/Auth/Contracts/AuthContracts.cs new file mode 100644 index 0000000..ed268f3 --- /dev/null +++ b/apps/backend/Auth/Contracts/AuthContracts.cs @@ -0,0 +1,42 @@ +namespace SpaceGame.Api.Auth.Contracts; + +public sealed class RegisterRequest +{ + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} + +public sealed class LoginRequest +{ + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} + +public sealed class RefreshTokenRequest +{ + public string RefreshToken { get; set; } = string.Empty; +} + +public sealed class ForgotPasswordRequest +{ + public string Email { get; set; } = string.Empty; +} + +public sealed class ResetPasswordRequest +{ + public string Token { get; set; } = string.Empty; + public string NewPassword { get; set; } = string.Empty; +} + +public sealed record AuthSessionResponse( + Guid UserId, + string Email, + IReadOnlyList Roles, + string AccessToken, + DateTimeOffset AccessTokenExpiresAtUtc, + string RefreshToken, + DateTimeOffset RefreshTokenExpiresAtUtc); + +public sealed record ForgotPasswordResponse( + bool Accepted, + string? ResetToken = null); diff --git a/apps/backend/Auth/Runtime/AuthRuntimeModels.cs b/apps/backend/Auth/Runtime/AuthRuntimeModels.cs new file mode 100644 index 0000000..47bd5f8 --- /dev/null +++ b/apps/backend/Auth/Runtime/AuthRuntimeModels.cs @@ -0,0 +1,24 @@ +namespace SpaceGame.Api.Auth.Runtime; + +public sealed record UserAccount( + Guid Id, + string Email, + string PasswordHash, + DateTimeOffset CreatedAtUtc, + IReadOnlyList Roles); + +public sealed record RefreshTokenRecord( + Guid Id, + Guid UserId, + string TokenHash, + DateTimeOffset CreatedAtUtc, + DateTimeOffset ExpiresAtUtc, + DateTimeOffset? RevokedAtUtc); + +public sealed record PasswordResetTokenRecord( + Guid Id, + Guid UserId, + string TokenHash, + DateTimeOffset CreatedAtUtc, + DateTimeOffset ExpiresAtUtc, + DateTimeOffset? ConsumedAtUtc); diff --git a/apps/backend/Auth/Simulation/AuthOptions.cs b/apps/backend/Auth/Simulation/AuthOptions.cs new file mode 100644 index 0000000..3c36dfc --- /dev/null +++ b/apps/backend/Auth/Simulation/AuthOptions.cs @@ -0,0 +1,14 @@ +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class AuthOptions +{ + public string ConnectionString { get; set; } = string.Empty; + public List DevSeedUsers { get; set; } = []; +} + +public sealed class SeedUserOptions +{ + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public List Roles { get; set; } = []; +} diff --git a/apps/backend/Auth/Simulation/AuthPolicyNames.cs b/apps/backend/Auth/Simulation/AuthPolicyNames.cs new file mode 100644 index 0000000..8d04281 --- /dev/null +++ b/apps/backend/Auth/Simulation/AuthPolicyNames.cs @@ -0,0 +1,13 @@ +namespace SpaceGame.Api.Auth.Simulation; + +public static class AuthPolicyNames +{ + public const string AdminAccess = "AdminAccess"; + public const string GmAccess = "GmAccess"; +} + +public static class AuthRoleNames +{ + public const string Gm = "gm"; + public const string Admin = "admin"; +} diff --git a/apps/backend/Auth/Simulation/AuthSchemaInitializer.cs b/apps/backend/Auth/Simulation/AuthSchemaInitializer.cs new file mode 100644 index 0000000..3981d30 --- /dev/null +++ b/apps/backend/Auth/Simulation/AuthSchemaInitializer.cs @@ -0,0 +1,41 @@ +using Npgsql; + +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class AuthSchemaInitializer(NpgsqlDataSource dataSource) +{ + public async Task EnsureSchemaAsync(CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + create table if not exists auth_users ( + id uuid primary key, + email text not null unique, + password_hash text not null, + created_at_utc timestamptz not null, + roles text[] not null default '{}' + ); + + alter table auth_users + add column if not exists roles text[] not null default '{}'; + + create table if not exists auth_refresh_tokens ( + id uuid primary key, + user_id uuid not null references auth_users(id) on delete cascade, + token_hash text not null unique, + created_at_utc timestamptz not null, + expires_at_utc timestamptz not null, + revoked_at_utc timestamptz null + ); + + create table if not exists auth_password_reset_tokens ( + id uuid primary key, + user_id uuid not null references auth_users(id) on delete cascade, + token_hash text not null unique, + created_at_utc timestamptz not null, + expires_at_utc timestamptz not null, + consumed_at_utc timestamptz null + ); + """); + await command.ExecuteNonQueryAsync(cancellationToken); + } +} diff --git a/apps/backend/Auth/Simulation/AuthService.cs b/apps/backend/Auth/Simulation/AuthService.cs new file mode 100644 index 0000000..8f86cb0 --- /dev/null +++ b/apps/backend/Auth/Simulation/AuthService.cs @@ -0,0 +1,120 @@ +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class AuthService( + IAuthRepository authRepository, + LocalPasswordHasher passwordHasher, + ITokenService tokenService, + RefreshTokenFactory refreshTokenFactory, + IPasswordResetDelivery passwordResetDelivery) +{ + public async Task RegisterAsync(RegisterRequest request, CancellationToken cancellationToken) + { + var email = NormalizeEmail(request.Email); + ValidatePassword(request.Password); + + if (await authRepository.FindUserByEmailAsync(email, cancellationToken) is not null) + { + throw new InvalidOperationException("An account already exists for that email."); + } + + var user = await authRepository.CreateUserAsync(email, passwordHasher.HashPassword(request.Password), [], cancellationToken); + return await CreateSessionAsync(user, cancellationToken); + } + + public async Task LoginAsync(LoginRequest request, CancellationToken cancellationToken) + { + var email = NormalizeEmail(request.Email); + var user = await authRepository.FindUserByEmailAsync(email, cancellationToken) + ?? throw new InvalidOperationException("Invalid email or password."); + if (!passwordHasher.VerifyPassword(request.Password, user.PasswordHash)) + { + throw new InvalidOperationException("Invalid email or password."); + } + + return await CreateSessionAsync(user, cancellationToken); + } + + public async Task RefreshAsync(RefreshTokenRequest request, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(request.RefreshToken)) + { + throw new InvalidOperationException("Refresh token is required."); + } + + var tokenHash = refreshTokenFactory.HashToken(request.RefreshToken); + var record = await authRepository.FindRefreshTokenAsync(tokenHash, cancellationToken) + ?? throw new InvalidOperationException("Refresh token is invalid."); + if (record.RevokedAtUtc is not null || record.ExpiresAtUtc <= DateTimeOffset.UtcNow) + { + throw new InvalidOperationException("Refresh token is expired."); + } + + var user = await authRepository.FindUserByIdAsync(record.UserId, cancellationToken) + ?? throw new InvalidOperationException("User account was not found."); + await authRepository.RevokeRefreshTokenAsync(record.Id, cancellationToken); + return await CreateSessionAsync(user, cancellationToken); + } + + public async Task ForgotPasswordAsync(ForgotPasswordRequest request, CancellationToken cancellationToken) + { + var email = NormalizeEmail(request.Email); + var user = await authRepository.FindUserByEmailAsync(email, cancellationToken); + if (user is null) + { + return new ForgotPasswordResponse(true); + } + + var resetToken = refreshTokenFactory.CreateToken(); + var resetTokenHash = refreshTokenFactory.HashToken(resetToken); + var expiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(30); + await authRepository.StorePasswordResetTokenAsync(user.Id, resetTokenHash, expiresAtUtc, cancellationToken); + return await passwordResetDelivery.DeliverAsync(user, resetToken, cancellationToken); + } + + public async Task ResetPasswordAsync(ResetPasswordRequest request, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(request.Token)) + { + throw new InvalidOperationException("Reset token is required."); + } + + ValidatePassword(request.NewPassword); + var tokenHash = refreshTokenFactory.HashToken(request.Token); + var record = await authRepository.FindPasswordResetTokenAsync(tokenHash, cancellationToken) + ?? throw new InvalidOperationException("Reset token is invalid."); + if (record.ConsumedAtUtc is not null || record.ExpiresAtUtc <= DateTimeOffset.UtcNow) + { + throw new InvalidOperationException("Reset token is expired."); + } + + await authRepository.UpdatePasswordHashAsync(record.UserId, passwordHasher.HashPassword(request.NewPassword), cancellationToken); + await authRepository.ConsumePasswordResetTokenAsync(record.Id, cancellationToken); + await authRepository.RevokeAllRefreshTokensAsync(record.UserId, cancellationToken); + } + + private async Task CreateSessionAsync(UserAccount user, CancellationToken cancellationToken) + { + var (accessToken, accessExpiresAtUtc) = tokenService.CreateAccessToken(user); + var (refreshToken, refreshTokenHash, refreshExpiresAtUtc) = tokenService.CreateRefreshToken(); + await authRepository.StoreRefreshTokenAsync(user.Id, refreshTokenHash, refreshExpiresAtUtc, cancellationToken); + return new AuthSessionResponse(user.Id, user.Email, user.Roles, accessToken, accessExpiresAtUtc, refreshToken, refreshExpiresAtUtc); + } + + private static string NormalizeEmail(string email) + { + if (string.IsNullOrWhiteSpace(email)) + { + throw new InvalidOperationException("Email is required."); + } + + return email.Trim().ToLowerInvariant(); + } + + private static void ValidatePassword(string password) + { + if (string.IsNullOrWhiteSpace(password) || password.Length < 8) + { + throw new InvalidOperationException("Password must be at least 8 characters."); + } + } +} diff --git a/apps/backend/Auth/Simulation/DevAuthSeeder.cs b/apps/backend/Auth/Simulation/DevAuthSeeder.cs new file mode 100644 index 0000000..12a360c --- /dev/null +++ b/apps/backend/Auth/Simulation/DevAuthSeeder.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class DevAuthSeeder( + IHostEnvironment hostEnvironment, + IOptions authOptions, + IAuthRepository authRepository, + LocalPasswordHasher passwordHasher) +{ + public async Task SeedAsync(CancellationToken cancellationToken) + { + if (!hostEnvironment.IsDevelopment()) + { + return; + } + + foreach (var seedUser in authOptions.Value.DevSeedUsers) + { + if (string.IsNullOrWhiteSpace(seedUser.Email) || string.IsNullOrWhiteSpace(seedUser.Password)) + { + continue; + } + + await authRepository.UpsertUserAsync( + seedUser.Email.Trim().ToLowerInvariant(), + passwordHasher.HashPassword(seedUser.Password), + seedUser.Roles, + cancellationToken); + } + } +} diff --git a/apps/backend/Auth/Simulation/DevPasswordResetDelivery.cs b/apps/backend/Auth/Simulation/DevPasswordResetDelivery.cs new file mode 100644 index 0000000..78386a3 --- /dev/null +++ b/apps/backend/Auth/Simulation/DevPasswordResetDelivery.cs @@ -0,0 +1,7 @@ +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class DevPasswordResetDelivery : IPasswordResetDelivery +{ + public Task DeliverAsync(UserAccount user, string resetToken, CancellationToken cancellationToken) => + Task.FromResult(new ForgotPasswordResponse(true, resetToken)); +} diff --git a/apps/backend/Auth/Simulation/HttpContextPlayerIdentityResolver.cs b/apps/backend/Auth/Simulation/HttpContextPlayerIdentityResolver.cs new file mode 100644 index 0000000..4488df2 --- /dev/null +++ b/apps/backend/Auth/Simulation/HttpContextPlayerIdentityResolver.cs @@ -0,0 +1,23 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class HttpContextPlayerIdentityResolver(IHttpContextAccessor httpContextAccessor) : IPlayerIdentityResolver +{ + public Guid? GetCurrentPlayerId() + { + var subject = httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier) + ?? httpContextAccessor.HttpContext?.User.FindFirstValue("sub"); + return Guid.TryParse(subject, out var playerId) ? playerId : null; + } + + public Guid GetRequiredPlayerId() => + GetCurrentPlayerId() ?? throw new InvalidOperationException("Authenticated player identity is required."); + + public bool CanAccessGm() + { + var user = httpContextAccessor.HttpContext?.User; + return user?.IsInRole("gm") == true || user?.IsInRole("admin") == true; + } +} diff --git a/apps/backend/Auth/Simulation/IAuthRepository.cs b/apps/backend/Auth/Simulation/IAuthRepository.cs new file mode 100644 index 0000000..f493e74 --- /dev/null +++ b/apps/backend/Auth/Simulation/IAuthRepository.cs @@ -0,0 +1,17 @@ +namespace SpaceGame.Api.Auth.Simulation; + +public interface IAuthRepository +{ + Task FindUserByEmailAsync(string email, CancellationToken cancellationToken); + Task FindUserByIdAsync(Guid userId, CancellationToken cancellationToken); + Task CreateUserAsync(string email, string passwordHash, IReadOnlyCollection roles, CancellationToken cancellationToken); + Task UpsertUserAsync(string email, string passwordHash, IReadOnlyCollection roles, CancellationToken cancellationToken); + Task StoreRefreshTokenAsync(Guid userId, string tokenHash, DateTimeOffset expiresAtUtc, CancellationToken cancellationToken); + Task FindRefreshTokenAsync(string tokenHash, CancellationToken cancellationToken); + Task RevokeRefreshTokenAsync(Guid refreshTokenId, CancellationToken cancellationToken); + Task RevokeAllRefreshTokensAsync(Guid userId, CancellationToken cancellationToken); + Task StorePasswordResetTokenAsync(Guid userId, string tokenHash, DateTimeOffset expiresAtUtc, CancellationToken cancellationToken); + Task FindPasswordResetTokenAsync(string tokenHash, CancellationToken cancellationToken); + Task ConsumePasswordResetTokenAsync(Guid passwordResetTokenId, CancellationToken cancellationToken); + Task UpdatePasswordHashAsync(Guid userId, string passwordHash, CancellationToken cancellationToken); +} diff --git a/apps/backend/Auth/Simulation/IPasswordResetDelivery.cs b/apps/backend/Auth/Simulation/IPasswordResetDelivery.cs new file mode 100644 index 0000000..bd93d9a --- /dev/null +++ b/apps/backend/Auth/Simulation/IPasswordResetDelivery.cs @@ -0,0 +1,6 @@ +namespace SpaceGame.Api.Auth.Simulation; + +public interface IPasswordResetDelivery +{ + Task DeliverAsync(UserAccount user, string resetToken, CancellationToken cancellationToken); +} diff --git a/apps/backend/Auth/Simulation/IPlayerIdentityResolver.cs b/apps/backend/Auth/Simulation/IPlayerIdentityResolver.cs new file mode 100644 index 0000000..0a6e119 --- /dev/null +++ b/apps/backend/Auth/Simulation/IPlayerIdentityResolver.cs @@ -0,0 +1,8 @@ +namespace SpaceGame.Api.Auth.Simulation; + +public interface IPlayerIdentityResolver +{ + Guid? GetCurrentPlayerId(); + Guid GetRequiredPlayerId(); + bool CanAccessGm(); +} diff --git a/apps/backend/Auth/Simulation/ITokenService.cs b/apps/backend/Auth/Simulation/ITokenService.cs new file mode 100644 index 0000000..a7d039d --- /dev/null +++ b/apps/backend/Auth/Simulation/ITokenService.cs @@ -0,0 +1,7 @@ +namespace SpaceGame.Api.Auth.Simulation; + +public interface ITokenService +{ + (string Token, DateTimeOffset ExpiresAtUtc) CreateAccessToken(UserAccount user); + (string Token, string TokenHash, DateTimeOffset ExpiresAtUtc) CreateRefreshToken(); +} diff --git a/apps/backend/Auth/Simulation/JwtOptions.cs b/apps/backend/Auth/Simulation/JwtOptions.cs new file mode 100644 index 0000000..2201d6d --- /dev/null +++ b/apps/backend/Auth/Simulation/JwtOptions.cs @@ -0,0 +1,10 @@ +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class JwtOptions +{ + public string Issuer { get; set; } = "space-game"; + public string Audience { get; set; } = "space-game-viewer"; + public string SigningKey { get; set; } = string.Empty; + public int AccessTokenLifetimeMinutes { get; set; } = 30; + public int RefreshTokenLifetimeDays { get; set; } = 30; +} diff --git a/apps/backend/Auth/Simulation/JwtTokenService.cs b/apps/backend/Auth/Simulation/JwtTokenService.cs new file mode 100644 index 0000000..f312e14 --- /dev/null +++ b/apps/backend/Auth/Simulation/JwtTokenService.cs @@ -0,0 +1,51 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class JwtTokenService( + IOptions jwtOptions, + RefreshTokenFactory refreshTokenFactory) : ITokenService +{ + public (string Token, DateTimeOffset ExpiresAtUtc) CreateAccessToken(UserAccount user) + { + var options = jwtOptions.Value; + var expiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(Math.Max(options.AccessTokenLifetimeMinutes, 5)); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.SigningKey)); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), + new Claim(JwtRegisteredClaimNames.Email, user.Email), + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Email, user.Email), + }.ToList(); + + foreach (var role in user.Roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + claims.Add(new Claim("role", role)); + } + + var token = new JwtSecurityToken( + issuer: options.Issuer, + audience: options.Audience, + claims: claims, + notBefore: DateTime.UtcNow, + expires: expiresAtUtc.UtcDateTime, + signingCredentials: credentials); + + return (new JwtSecurityTokenHandler().WriteToken(token), expiresAtUtc); + } + + public (string Token, string TokenHash, DateTimeOffset ExpiresAtUtc) CreateRefreshToken() + { + var token = refreshTokenFactory.CreateToken(); + var tokenHash = refreshTokenFactory.HashToken(token); + var expiresAtUtc = DateTimeOffset.UtcNow.AddDays(Math.Max(jwtOptions.Value.RefreshTokenLifetimeDays, 1)); + return (token, tokenHash, expiresAtUtc); + } +} diff --git a/apps/backend/Auth/Simulation/LocalPasswordHasher.cs b/apps/backend/Auth/Simulation/LocalPasswordHasher.cs new file mode 100644 index 0000000..04b23e2 --- /dev/null +++ b/apps/backend/Auth/Simulation/LocalPasswordHasher.cs @@ -0,0 +1,42 @@ +using System.Security.Cryptography; + +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class LocalPasswordHasher +{ + private const int SaltSize = 16; + private const int KeySize = 32; + private const int IterationCount = 120_000; + + public string HashPassword(string password) + { + ArgumentException.ThrowIfNullOrWhiteSpace(password); + + Span salt = stackalloc byte[SaltSize]; + RandomNumberGenerator.Fill(salt); + var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, IterationCount, HashAlgorithmName.SHA256, KeySize); + return $"pbkdf2-sha256${IterationCount}${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}"; + } + + public bool VerifyPassword(string password, string encodedHash) + { + ArgumentException.ThrowIfNullOrWhiteSpace(password); + ArgumentException.ThrowIfNullOrWhiteSpace(encodedHash); + + var parts = encodedHash.Split('$', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 4 || !string.Equals(parts[0], "pbkdf2-sha256", StringComparison.Ordinal)) + { + return false; + } + + if (!int.TryParse(parts[1], out var iterations)) + { + return false; + } + + var salt = Convert.FromBase64String(parts[2]); + var expected = Convert.FromBase64String(parts[3]); + var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, expected.Length); + return CryptographicOperations.FixedTimeEquals(actual, expected); + } +} diff --git a/apps/backend/Auth/Simulation/PostgresAuthRepository.cs b/apps/backend/Auth/Simulation/PostgresAuthRepository.cs new file mode 100644 index 0000000..ea9e3ce --- /dev/null +++ b/apps/backend/Auth/Simulation/PostgresAuthRepository.cs @@ -0,0 +1,199 @@ +using Npgsql; + +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class PostgresAuthRepository(NpgsqlDataSource dataSource) : IAuthRepository +{ + public async Task FindUserByEmailAsync(string email, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + select id, email, password_hash, created_at_utc, roles + from auth_users + where email = $1 + """); + command.Parameters.AddWithValue(email); + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + return await reader.ReadAsync(cancellationToken) ? ReadUser(reader) : null; + } + + public async Task FindUserByIdAsync(Guid userId, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + select id, email, password_hash, created_at_utc, roles + from auth_users + where id = $1 + """); + command.Parameters.AddWithValue(userId); + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + return await reader.ReadAsync(cancellationToken) ? ReadUser(reader) : null; + } + + public async Task CreateUserAsync(string email, string passwordHash, IReadOnlyCollection roles, CancellationToken cancellationToken) + { + var userId = Guid.NewGuid(); + var createdAtUtc = DateTimeOffset.UtcNow; + await using var command = dataSource.CreateCommand(""" + insert into auth_users (id, email, password_hash, created_at_utc, roles) + values ($1, $2, $3, $4, $5) + """); + command.Parameters.AddWithValue(userId); + command.Parameters.AddWithValue(email); + command.Parameters.AddWithValue(passwordHash); + command.Parameters.AddWithValue(createdAtUtc); + command.Parameters.AddWithValue(roles.Select(role => role.Trim().ToLowerInvariant()).Where(role => role.Length > 0).Distinct(StringComparer.Ordinal).ToArray()); + await command.ExecuteNonQueryAsync(cancellationToken); + return new UserAccount(userId, email, passwordHash, createdAtUtc, roles.Select(role => role.Trim().ToLowerInvariant()).Where(role => role.Length > 0).Distinct(StringComparer.Ordinal).ToArray()); + } + + public async Task UpsertUserAsync(string email, string passwordHash, IReadOnlyCollection roles, CancellationToken cancellationToken) + { + var normalizedRoles = roles.Select(role => role.Trim().ToLowerInvariant()).Where(role => role.Length > 0).Distinct(StringComparer.Ordinal).ToArray(); + var userId = Guid.NewGuid(); + var createdAtUtc = DateTimeOffset.UtcNow; + await using var command = dataSource.CreateCommand(""" + insert into auth_users (id, email, password_hash, created_at_utc, roles) + values ($1, $2, $3, $4, $5) + on conflict (email) do update + set password_hash = excluded.password_hash, + roles = excluded.roles + returning id, email, password_hash, created_at_utc, roles + """); + command.Parameters.AddWithValue(userId); + command.Parameters.AddWithValue(email); + command.Parameters.AddWithValue(passwordHash); + command.Parameters.AddWithValue(createdAtUtc); + command.Parameters.AddWithValue(normalizedRoles); + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + await reader.ReadAsync(cancellationToken); + return ReadUser(reader); + } + + public async Task StoreRefreshTokenAsync(Guid userId, string tokenHash, DateTimeOffset expiresAtUtc, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + insert into auth_refresh_tokens (id, user_id, token_hash, created_at_utc, expires_at_utc, revoked_at_utc) + values ($1, $2, $3, $4, $5, null) + """); + command.Parameters.AddWithValue(Guid.NewGuid()); + command.Parameters.AddWithValue(userId); + command.Parameters.AddWithValue(tokenHash); + command.Parameters.AddWithValue(DateTimeOffset.UtcNow); + command.Parameters.AddWithValue(expiresAtUtc); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + public async Task FindRefreshTokenAsync(string tokenHash, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + select id, user_id, token_hash, created_at_utc, expires_at_utc, revoked_at_utc + from auth_refresh_tokens + where token_hash = $1 + """); + command.Parameters.AddWithValue(tokenHash); + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + if (!await reader.ReadAsync(cancellationToken)) + { + return null; + } + + return new RefreshTokenRecord( + reader.GetGuid(0), + reader.GetGuid(1), + reader.GetString(2), + reader.GetFieldValue(3), + reader.GetFieldValue(4), + reader.IsDBNull(5) ? null : reader.GetFieldValue(5)); + } + + public async Task RevokeRefreshTokenAsync(Guid refreshTokenId, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + update auth_refresh_tokens + set revoked_at_utc = $2 + where id = $1 and revoked_at_utc is null + """); + command.Parameters.AddWithValue(refreshTokenId); + command.Parameters.AddWithValue(DateTimeOffset.UtcNow); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + public async Task RevokeAllRefreshTokensAsync(Guid userId, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + update auth_refresh_tokens + set revoked_at_utc = $2 + where user_id = $1 and revoked_at_utc is null + """); + command.Parameters.AddWithValue(userId); + command.Parameters.AddWithValue(DateTimeOffset.UtcNow); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + public async Task StorePasswordResetTokenAsync(Guid userId, string tokenHash, DateTimeOffset expiresAtUtc, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + insert into auth_password_reset_tokens (id, user_id, token_hash, created_at_utc, expires_at_utc, consumed_at_utc) + values ($1, $2, $3, $4, $5, null) + """); + command.Parameters.AddWithValue(Guid.NewGuid()); + command.Parameters.AddWithValue(userId); + command.Parameters.AddWithValue(tokenHash); + command.Parameters.AddWithValue(DateTimeOffset.UtcNow); + command.Parameters.AddWithValue(expiresAtUtc); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + public async Task FindPasswordResetTokenAsync(string tokenHash, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + select id, user_id, token_hash, created_at_utc, expires_at_utc, consumed_at_utc + from auth_password_reset_tokens + where token_hash = $1 + """); + command.Parameters.AddWithValue(tokenHash); + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + if (!await reader.ReadAsync(cancellationToken)) + { + return null; + } + + return new PasswordResetTokenRecord( + reader.GetGuid(0), + reader.GetGuid(1), + reader.GetString(2), + reader.GetFieldValue(3), + reader.GetFieldValue(4), + reader.IsDBNull(5) ? null : reader.GetFieldValue(5)); + } + + public async Task ConsumePasswordResetTokenAsync(Guid passwordResetTokenId, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + update auth_password_reset_tokens + set consumed_at_utc = $2 + where id = $1 and consumed_at_utc is null + """); + command.Parameters.AddWithValue(passwordResetTokenId); + command.Parameters.AddWithValue(DateTimeOffset.UtcNow); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + public async Task UpdatePasswordHashAsync(Guid userId, string passwordHash, CancellationToken cancellationToken) + { + await using var command = dataSource.CreateCommand(""" + update auth_users + set password_hash = $2 + where id = $1 + """); + command.Parameters.AddWithValue(userId); + command.Parameters.AddWithValue(passwordHash); + await command.ExecuteNonQueryAsync(cancellationToken); + } + + private static UserAccount ReadUser(NpgsqlDataReader reader) => new( + reader.GetGuid(0), + reader.GetString(1), + reader.GetString(2), + reader.GetFieldValue(3), + reader.GetFieldValue(4)); +} diff --git a/apps/backend/Auth/Simulation/RefreshTokenFactory.cs b/apps/backend/Auth/Simulation/RefreshTokenFactory.cs new file mode 100644 index 0000000..c3e8d08 --- /dev/null +++ b/apps/backend/Auth/Simulation/RefreshTokenFactory.cs @@ -0,0 +1,21 @@ +using System.Security.Cryptography; +using System.Text; + +namespace SpaceGame.Api.Auth.Simulation; + +public sealed class RefreshTokenFactory +{ + public string CreateToken() + { + Span bytes = stackalloc byte[32]; + RandomNumberGenerator.Fill(bytes); + return Convert.ToBase64String(bytes); + } + + public string HashToken(string token) + { + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(token)); + return Convert.ToHexString(bytes); + } +} diff --git a/apps/backend/Definitions/WorldDefinitions.cs b/apps/backend/Definitions/WorldDefinitions.cs index 28010bf..ea9fdcf 100644 --- a/apps/backend/Definitions/WorldDefinitions.cs +++ b/apps/backend/Definitions/WorldDefinitions.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using SpaceGame.Api.Shared.Runtime; +using SpaceGame.Api.Shared.Runtime; using SpaceGame.Api.Universe.Simulation; namespace SpaceGame.Api.Definitions; @@ -368,6 +369,72 @@ public sealed class PlanetDefinition public bool HasRing { get; set; } } +public enum ShipPurpose +{ + [JsonStringEnumMemberName("auxiliary")] + Auxiliary, + [JsonStringEnumMemberName("mine")] + Mine, + [JsonStringEnumMemberName("build")] + Build, + [JsonStringEnumMemberName("fight")] + Fight, + [JsonStringEnumMemberName("trade")] + Trade, + [JsonStringEnumMemberName("salvage")] + Salvage, + [JsonStringEnumMemberName("dismantling")] + Dismantling, +} + +public enum ShipType +{ + [JsonStringEnumMemberName("resupplier")] + Resupplier, + [JsonStringEnumMemberName("miner")] + Miner, + [JsonStringEnumMemberName("carrier")] + Carrier, + [JsonStringEnumMemberName("fighter")] + Fighter, + [JsonStringEnumMemberName("heavyfighter")] + HeavyFighter, + [JsonStringEnumMemberName("destroyer")] + Destroyer, + [JsonStringEnumMemberName("largeminer")] + LargeMiner, + [JsonStringEnumMemberName("freighter")] + Freighter, + [JsonStringEnumMemberName("bomber")] + Bomber, + [JsonStringEnumMemberName("scavenger")] + Scavenger, + [JsonStringEnumMemberName("frigate")] + Frigate, + [JsonStringEnumMemberName("transporter")] + Transporter, + [JsonStringEnumMemberName("interceptor")] + Interceptor, + [JsonStringEnumMemberName("scout")] + Scout, + [JsonStringEnumMemberName("courier")] + Courier, + [JsonStringEnumMemberName("builder")] + Builder, + [JsonStringEnumMemberName("corvette")] + Corvette, + [JsonStringEnumMemberName("police")] + Police, + [JsonStringEnumMemberName("battleship")] + Battleship, + [JsonStringEnumMemberName("gunboat")] + Gunboat, + [JsonStringEnumMemberName("tug")] + Tug, + [JsonStringEnumMemberName("compactor")] + Compactor, +} + public sealed class ShipDefinition { public required string Id { get; set; } @@ -379,9 +446,9 @@ public sealed class ShipDefinition public float Hull { get; set; } public Dictionary Storage { get; set; } = new(StringComparer.Ordinal); public int People { get; set; } - public string Purpose { get; set; } = string.Empty; + public ShipPurpose Purpose { get; set; } public string Thruster { get; set; } = string.Empty; - public string Type { get; set; } = string.Empty; + public ShipType Type { get; set; } public float Mass { get; set; } public ShipInertiaDefinition? Inertia { get; set; } public ShipDragDefinition? Drag { get; set; } @@ -395,12 +462,6 @@ public sealed class ShipDefinition public ItemPriceDefinition? Price { get; set; } public List Production { get; set; } = []; [JsonIgnore] - public string Label => Name; - [JsonIgnore] - public string Kind => InferKind(Purpose); - [JsonIgnore] - public string Class => Type; - [JsonIgnore] public float Speed => InferLocalSpeed(Size); [JsonIgnore] public float WarpSpeed => InferWarpSpeed(Size); @@ -408,53 +469,15 @@ public sealed class ShipDefinition public float FtlSpeed => InferFtlSpeed(Size); [JsonIgnore] public float SpoolTime => InferSpoolTime(Size); - [JsonIgnore] - public float CargoCapacity => Cargo.Sum(entry => entry.Max); - [JsonIgnore] - public StorageKind? CargoKind => Cargo - .SelectMany(entry => entry.Types) - .Select(type => type.ToNullableStorageKind()) - .FirstOrDefault(kind => kind is not null); - [JsonIgnore] - public float MaxHealth => Hull; - [JsonIgnore] - public IReadOnlyList Capabilities => InferCapabilities(Purpose, Type, Cargo, Turrets); + public float GetTotalCargoCapacity() => Cargo.Sum(entry => entry.Max); - private static string InferKind(string purpose) => - purpose switch - { - "build" => "construction", - "trade" => "transport", - "mine" => "mining", - "fight" => "military", - "auxiliary" => "military", - _ => purpose, - }; + public float GetCargoCapacity(StorageKind kind) => + Cargo + .Where(entry => entry.Types.Any(type => type.ToNullableStorageKind() == kind)) + .Sum(entry => entry.Max); - private static List InferCapabilities( - string purpose, - string type, - IReadOnlyCollection cargo, - IReadOnlyCollection turrets) - { - var capabilities = new List { "warp", "ftl" }; - - if (string.Equals(purpose, "mine", StringComparison.Ordinal) - || type.Contains("miner", StringComparison.Ordinal) - || turrets.Any(turret => turret.Types.Contains("mining", StringComparer.Ordinal))) - { - capabilities.Add("mining"); - } - - if (cargo.Any(entry => entry.Types.Contains("container", StringComparer.Ordinal) - || entry.Types.Contains("solid", StringComparer.Ordinal) - || entry.Types.Contains("liquid", StringComparer.Ordinal))) - { - capabilities.Add("cargo"); - } - - return capabilities; - } + public bool SupportsCargoKind(StorageKind kind) => + GetCargoCapacity(kind) > 0f; private static float InferWarpSpeed(string size) => size switch diff --git a/apps/backend/Factions/AI/CommanderPlanningService.cs b/apps/backend/Factions/AI/CommanderPlanningService.cs index 3ea4445..4c439be 100644 --- a/apps/backend/Factions/AI/CommanderPlanningService.cs +++ b/apps/backend/Factions/AI/CommanderPlanningService.cs @@ -1,5 +1,6 @@ using SpaceGame.Api.Industry.Planning; using SpaceGame.Api.Stations.Simulation; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Factions.AI; @@ -13,8 +14,12 @@ internal sealed class CommanderPlanningService private const int MaxDecisionLogEntries = 40; private const int MaxOutcomeEntries = 32; private const int MaxAiOrdersPerShip = 2; + private const string MilitaryShipCategory = "military"; + private const string MiningShipCategory = "mining"; + private const string TransportShipCategory = "transport"; + private const string ConstructionShipCategory = "construction"; - internal void UpdateCommanders(SimulationWorld world, float deltaSeconds, ICollection events) + internal void UpdateCommanders(SimulationWorld world, IPlayerStateStore playerStateStore, float deltaSeconds, ICollection events) { EnsureHierarchy(world); @@ -33,7 +38,7 @@ internal sealed class CommanderPlanningService foreach (var commander in world.Commanders.Where(candidate => candidate.IsAlive && candidate.Kind == CommanderKind.Faction).ToList()) { - if (PlayerFactionService.IsPlayerFaction(world, commander.FactionId)) + if (PlayerFactionService.IsPlayerFaction(playerStateStore, commander.FactionId)) { continue; } @@ -48,7 +53,7 @@ internal sealed class CommanderPlanningService foreach (var commander in world.Commanders.Where(candidate => candidate.IsAlive && candidate.Kind == CommanderKind.Fleet).ToList()) { - if (PlayerFactionService.IsPlayerFaction(world, commander.FactionId)) + if (PlayerFactionService.IsPlayerFaction(playerStateStore, commander.FactionId)) { continue; } @@ -63,7 +68,7 @@ internal sealed class CommanderPlanningService foreach (var commander in world.Commanders.Where(candidate => candidate.IsAlive && candidate.Kind == CommanderKind.Station).ToList()) { - if (PlayerFactionService.IsPlayerFaction(world, commander.FactionId)) + if (PlayerFactionService.IsPlayerFaction(playerStateStore, commander.FactionId)) { continue; } @@ -78,7 +83,7 @@ internal sealed class CommanderPlanningService foreach (var commander in world.Commanders.Where(candidate => candidate.IsAlive && candidate.Kind == CommanderKind.Ship).ToList()) { - if (PlayerFactionService.IsPlayerFaction(world, commander.FactionId)) + if (PlayerFactionService.IsPlayerFaction(playerStateStore, commander.FactionId)) { continue; } @@ -268,7 +273,7 @@ internal sealed class CommanderPlanningService CommanderRuntime factionCommander, IReadOnlyDictionary stationCommanders) { - if (string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal)) + if (IsMilitaryShip(ship.Definition)) { return factionCommander; } @@ -456,8 +461,8 @@ internal sealed class CommanderPlanningService ship.Id, nextAssignment is null ? "assignment-cleared" : "assignment-updated", nextAssignment is null - ? $"{ship.Definition.Label} returned to default behavior." - : $"{ship.Definition.Label} assigned to {nextAssignment.Kind}.", + ? $"{ship.Definition.Name} returned to default behavior." + : $"{ship.Definition.Name} assigned to {nextAssignment.Kind}.", DateTimeOffset.UtcNow)); } } @@ -586,10 +591,10 @@ internal sealed class CommanderPlanningService var frontCount = Math.Max(1, threatAssessment.ThreatSignals.Count(signal => signal.ScopeKind is "controlled-system" or "contested-system") + (expansionProject is null ? 0 : 1)); - var militaryShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && ship.Definition.Kind == "military"); - var minerShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && HasShipCapabilities(ship.Definition, "mining")); - var transportShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && ship.Definition.Kind == "transport"); - var constructorShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && ship.Definition.Kind == "construction"); + var militaryShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && IsMilitaryShip(ship.Definition)); + var minerShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && IsMiningShip(ship.Definition)); + var transportShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && IsTransportShip(ship.Definition)); + var constructorShipCount = world.Ships.Count(ship => ship.FactionId == faction.Id && ship.Health > 0f && IsConstructionShip(ship.Definition)); var hasShipyard = world.Stations.Any(station => string.Equals(station.FactionId, faction.Id, StringComparison.Ordinal) && station.InstalledModules.Contains("module_gen_build_l_01", StringComparer.Ordinal)); @@ -1365,9 +1370,9 @@ internal sealed class CommanderPlanningService Id = $"{campaign.Id}-protect-station-{station.Id}", CampaignId = campaign.Id, TheaterId = theater?.Id, - Kind = "protect-station", + Kind = ProtectStation, DelegationKind = "ship", - BehaviorKind = "protect-station", + BehaviorKind = ProtectStation, Status = "active", Priority = campaign.Priority + 8f, HomeSystemId = station.SystemId, @@ -1389,7 +1394,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "patrol-front", DelegationKind = "ship", - BehaviorKind = "patrol", + BehaviorKind = Patrol, Status = "active", Priority = campaign.Priority + 2f, HomeSystemId = campaign.TargetSystemId, @@ -1414,7 +1419,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "police-front", DelegationKind = "ship", - BehaviorKind = "police", + BehaviorKind = Police, Status = "active", Priority = campaign.Priority + 1f, HomeSystemId = campaign.TargetSystemId, @@ -1454,7 +1459,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "strike-station", DelegationKind = "ship", - BehaviorKind = "attack-target", + BehaviorKind = AttackTarget, Status = "active", Priority = campaign.Priority + 10f, TargetSystemId = enemyStation.SystemId, @@ -1478,7 +1483,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "hold-front", DelegationKind = "ship", - BehaviorKind = "protect-position", + BehaviorKind = ProtectPosition, Status = "active", Priority = campaign.Priority + 3f, TargetSystemId = campaign.TargetSystemId, @@ -1500,7 +1505,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "fleet-sustainment", DelegationKind = "ship", - BehaviorKind = "supply-fleet", + BehaviorKind = SupplyFleet, Status = "active", Priority = campaign.Priority + 1.5f, HomeSystemId = campaign.TargetSystemId, @@ -1539,7 +1544,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "construct-site", DelegationKind = "ship", - BehaviorKind = "construct-station", + BehaviorKind = ConstructStation, Status = "active", Priority = campaign.Priority + 8f, HomeSystemId = expansionProject.SystemId, @@ -1564,7 +1569,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "supply-site", DelegationKind = "ship", - BehaviorKind = "find-build-tasks", + BehaviorKind = FindBuildTasks, Status = "active", Priority = campaign.Priority + 4f, HomeSystemId = expansionProject.SystemId, @@ -1589,7 +1594,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "guard-site", DelegationKind = "ship", - BehaviorKind = "protect-position", + BehaviorKind = ProtectPosition, Status = "active", Priority = campaign.Priority + 2f, TargetSystemId = expansionProject.SystemId, @@ -1614,7 +1619,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "mine-expansion-input", DelegationKind = "ship", - BehaviorKind = "expert-auto-mine", + BehaviorKind = ExpertAutoMine, Status = "active", Priority = campaign.Priority + 1f, HomeSystemId = expansionProject.SystemId, @@ -1655,7 +1660,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "trade-shortage", DelegationKind = "ship", - BehaviorKind = "fill-shortages", + BehaviorKind = FillShortages, Status = "active", Priority = campaign.Priority + 5f, HomeSystemId = anchorStation?.SystemId, @@ -1680,7 +1685,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "mine-shortage", DelegationKind = "ship", - BehaviorKind = "expert-auto-mine", + BehaviorKind = ExpertAutoMine, Status = "active", Priority = campaign.Priority + 3f, HomeSystemId = anchorStation?.SystemId, @@ -1703,7 +1708,7 @@ internal sealed class CommanderPlanningService TheaterId = theater?.Id, Kind = "revisit-stations", DelegationKind = "ship", - BehaviorKind = "revisit-known-stations", + BehaviorKind = RevisitKnownStations, Status = "active", Priority = campaign.Priority + 0.5f, HomeSystemId = anchorStation?.SystemId, @@ -1743,7 +1748,7 @@ internal sealed class CommanderPlanningService CampaignId = campaign.Id, Kind = "feed-shipyard", DelegationKind = "ship", - BehaviorKind = "fill-shortages", + BehaviorKind = FillShortages, Status = "active", Priority = campaign.Priority + 4f, HomeSystemId = shipyard.SystemId, @@ -1768,7 +1773,7 @@ internal sealed class CommanderPlanningService CampaignId = campaign.Id, Kind = "mine-bottleneck", DelegationKind = "ship", - BehaviorKind = "expert-auto-mine", + BehaviorKind = ExpertAutoMine, Status = "active", Priority = campaign.Priority + 2f, HomeSystemId = shipyard.SystemId, @@ -1838,7 +1843,9 @@ internal sealed class CommanderPlanningService var reservedCommanderIds = new HashSet(StringComparer.Ordinal); var availableMilitaryCommanders = commanders.Count(commander => commander.Kind == CommanderKind.Ship && - world.Ships.FirstOrDefault(ship => ship.Id == commander.ControlledEntityId) is { Definition.Kind: "military", Health: > 0f }); + world.Ships.FirstOrDefault(ship => ship.Id == commander.ControlledEntityId) is { } commanderShip + && commanderShip.Health > 0f + && IsMilitaryShip(commanderShip.Definition)); var committedMilitaryCommanders = 0; foreach (var objective in objectives @@ -1921,11 +1928,11 @@ internal sealed class CommanderPlanningService return objective.BehaviorKind switch { - "construct-station" => string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal), - "find-build-tasks" => string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal), - "fill-shortages" or "advanced-auto-trade" or "revisit-known-stations" or "supply-fleet" => string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal), - "local-auto-mine" or "advanced-auto-mine" or "expert-auto-mine" => HasShipCapabilities(ship.Definition, "mining"), - "patrol" or "police" or "protect-position" or "protect-ship" or "protect-station" or "attack-target" => string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal), + ConstructStation => IsConstructionShip(ship.Definition), + FindBuildTasks => IsTransportShip(ship.Definition), + FillShortages or AdvancedAutoTrade or RevisitKnownStations or SupplyFleet => IsTransportShip(ship.Definition), + LocalAutoMine or AdvancedAutoMine or ExpertAutoMine => IsMiningShip(ship.Definition), + Patrol or Police or ProtectPosition or ProtectShip or ProtectStation or AttackTarget => IsMilitaryShip(ship.Definition), _ => true, }; } @@ -1992,7 +1999,7 @@ internal sealed class CommanderPlanningService Kind = "military-fleet", Status = economicAssessment.MilitaryShipCount >= economicAssessment.TargetMilitaryShipCount ? "stable" : "active", Priority = 80f + (threatAssessment.ThreatSignals.Count * 4f), - ShipKind = "military", + ShipKind = MilitaryShipCategory, TargetCount = economicAssessment.TargetMilitaryShipCount, CurrentCount = economicAssessment.MilitaryShipCount, Notes = "Maintain enough military hulls for all active fronts.", @@ -2004,7 +2011,7 @@ internal sealed class CommanderPlanningService Kind = "mining-fleet", Status = economicAssessment.MinerShipCount >= economicAssessment.TargetMinerShipCount ? "stable" : "active", Priority = 60f, - ShipKind = "mining", + ShipKind = MiningShipCategory, TargetCount = economicAssessment.TargetMinerShipCount, CurrentCount = economicAssessment.MinerShipCount, Notes = "Maintain raw resource extraction capacity.", @@ -2016,7 +2023,7 @@ internal sealed class CommanderPlanningService Kind = "logistics-fleet", Status = economicAssessment.TransportShipCount >= economicAssessment.TargetTransportShipCount ? "stable" : "active", Priority = 62f, - ShipKind = "transport", + ShipKind = TransportShipCategory, TargetCount = economicAssessment.TargetTransportShipCount, CurrentCount = economicAssessment.TransportShipCount, Notes = "Maintain logistics throughput across stations and fronts.", @@ -2028,7 +2035,7 @@ internal sealed class CommanderPlanningService Kind = "construction-fleet", Status = economicAssessment.ConstructorShipCount >= economicAssessment.TargetConstructorShipCount ? "stable" : "active", Priority = expansionProject is null ? 35f : 68f, - ShipKind = "construction", + ShipKind = ConstructionShipCategory, TargetCount = economicAssessment.TargetConstructorShipCount, CurrentCount = economicAssessment.ConstructorShipCount, Notes = "Maintain construction capacity for frontier growth.", @@ -2347,10 +2354,10 @@ internal sealed class CommanderPlanningService Kind = "fleet-command", BehaviorKind = campaign.Kind switch { - "offense" => "attack-target", - "defense" => "protect-position", - "expansion" => "protect-position", - _ => "patrol", + "offense" => AttackTarget, + "defense" => ProtectPosition, + "expansion" => ProtectPosition, + _ => Patrol, }, Status = campaign.Status, Priority = campaign.Priority, @@ -2380,7 +2387,7 @@ internal sealed class CommanderPlanningService { ObjectiveId = $"objective-station-{station.Id}-ship-production", Kind = "ship-production-focus", - BehaviorKind = "fill-shortages", + BehaviorKind = FillShortages, Status = "active", Priority = 55f, HomeSystemId = station.SystemId, @@ -2399,7 +2406,7 @@ internal sealed class CommanderPlanningService { ObjectiveId = $"objective-station-{station.Id}-commodity-focus-{bottleneckItem}", Kind = "commodity-focus", - BehaviorKind = "fill-shortages", + BehaviorKind = FillShortages, Status = "active", Priority = 45f, HomeSystemId = station.SystemId, @@ -2418,7 +2425,7 @@ internal sealed class CommanderPlanningService { ObjectiveId = $"objective-station-{station.Id}-expansion-support", Kind = "expansion-support", - BehaviorKind = "find-build-tasks", + BehaviorKind = FindBuildTasks, Status = "active", Priority = 40f, HomeSystemId = station.SystemId, @@ -2435,7 +2442,7 @@ internal sealed class CommanderPlanningService { ObjectiveId = $"objective-station-{station.Id}-oversight", Kind = "station-oversight", - BehaviorKind = "fill-shortages", + BehaviorKind = FillShortages, Status = "active", Priority = 30f, HomeSystemId = station.SystemId, @@ -2460,7 +2467,7 @@ internal sealed class CommanderPlanningService faction.StrategicState.Objectives.Any(objective => objective.CampaignId == campaign.Id && objective.CommanderId is not null && - (IsCombatObjective(objective) || string.Equals(objective.BehaviorKind, "supply-fleet", StringComparison.Ordinal)))) + (IsCombatObjective(objective) || string.Equals(objective.BehaviorKind, SupplyFleet, StringComparison.Ordinal)))) .Select(campaign => campaign.Id) .ToHashSet(StringComparer.Ordinal); @@ -2510,10 +2517,10 @@ internal sealed class CommanderPlanningService Kind = "fleet-command", BehaviorKind = campaign.Kind switch { - "offense" => "attack-target", - "defense" => "protect-position", - "expansion" => "protect-position", - _ => "patrol", + "offense" => AttackTarget, + "defense" => ProtectPosition, + "expansion" => ProtectPosition, + _ => Patrol, }, Status = campaign.Status, Priority = campaign.Priority + 1f, @@ -2581,7 +2588,7 @@ internal sealed class CommanderPlanningService { if (objective?.CampaignId is not null && fleetCommanders.TryGetValue(objective.CampaignId, out var fleetCommander) - && (IsCombatObjective(objective) || string.Equals(objective.BehaviorKind, "supply-fleet", StringComparison.Ordinal))) + && (IsCombatObjective(objective) || string.Equals(objective.BehaviorKind, SupplyFleet, StringComparison.Ordinal))) { return fleetCommander.Id; } @@ -2598,25 +2605,39 @@ internal sealed class CommanderPlanningService private static DefaultBehaviorRuntime BuildFallbackBehavior(SimulationWorld world, ShipRuntime ship) { var homeStation = ResolveFallbackHomeStation(world, ship); - if (HasShipCapabilities(ship.Definition, "mining")) + if (IsMiningShip(ship.Definition)) { + if (homeStation is null) + { + return new DefaultBehaviorRuntime + { + Kind = LocalAutoMine, + HomeSystemId = ship.SystemId, + HomeStationId = null, + AreaSystemId = ship.SystemId, + ItemId = "ore", + Radius = 24f, + MaxSystemRange = 0, + }; + } + return new DefaultBehaviorRuntime { - Kind = ship.Definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine", + Kind = ship.Definition.GetTotalCargoCapacity() >= 120f ? ExpertAutoMine : AdvancedAutoMine, HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, - PreferredItemId = null, + ItemId = null, Radius = 24f, - MaxSystemRange = ship.Definition.CargoCapacity >= 120f ? 3 : 1, + MaxSystemRange = ship.Definition.GetTotalCargoCapacity() >= 120f ? 3 : 1, }; } - if (string.Equals(ship.Definition.Kind, "transport", StringComparison.Ordinal)) + if (IsTransportShip(ship.Definition)) { return new DefaultBehaviorRuntime { - Kind = "advanced-auto-trade", + Kind = AdvancedAutoTrade, HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, @@ -2625,11 +2646,11 @@ internal sealed class CommanderPlanningService }; } - if (string.Equals(ship.Definition.Kind, "construction", StringComparison.Ordinal)) + if (IsConstructionShip(ship.Definition)) { return new DefaultBehaviorRuntime { - Kind = "construct-station", + Kind = ConstructStation, HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, @@ -2638,13 +2659,13 @@ internal sealed class CommanderPlanningService }; } - if (string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal)) + if (IsMilitaryShip(ship.Definition)) { var anchor = homeStation?.Position ?? ship.Position; var patrolRadius = (homeStation?.Radius ?? 30f) + 90f; return new DefaultBehaviorRuntime { - Kind = "patrol", + Kind = Patrol, HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, @@ -2660,7 +2681,7 @@ internal sealed class CommanderPlanningService return new DefaultBehaviorRuntime { - Kind = "idle", + Kind = Idle, HomeSystemId = homeStation?.SystemId ?? ship.SystemId, HomeStationId = homeStation?.Id, AreaSystemId = homeStation?.SystemId ?? ship.SystemId, @@ -2684,15 +2705,15 @@ internal sealed class CommanderPlanningService var areaSystemId = objective.TargetSystemId ?? objective.HomeSystemId ?? fallback.AreaSystemId ?? ship.SystemId; var radius = objective.BehaviorKind switch { - "protect-position" or "protect-station" or "patrol" or "police" => MathF.Max(80f, fallback.Radius), - "follow-ship" or "protect-ship" => MathF.Max(18f, fallback.Radius * 0.6f), - "fill-shortages" or "advanced-auto-trade" or "find-build-tasks" => MathF.Max(20f, fallback.Radius), + ProtectPosition or ProtectStation or Patrol or Police => MathF.Max(80f, fallback.Radius), + FollowShip or ProtectShip => MathF.Max(18f, fallback.Radius * 0.6f), + FillShortages or AdvancedAutoTrade or FindBuildTasks => MathF.Max(20f, fallback.Radius), _ => fallback.Radius, }; var maxRange = objective.BehaviorKind switch { - "attack-target" or "protect-position" or "protect-station" or "protect-ship" or "patrol" or "police" => Math.Max(1, fallback.MaxSystemRange), - "fill-shortages" or "advanced-auto-trade" or "find-build-tasks" or "supply-fleet" => Math.Max(2, fallback.MaxSystemRange), + AttackTarget or ProtectPosition or ProtectStation or ProtectShip or Patrol or Police => Math.Max(1, fallback.MaxSystemRange), + FillShortages or AdvancedAutoTrade or FindBuildTasks or SupplyFleet => Math.Max(2, fallback.MaxSystemRange), _ => fallback.MaxSystemRange, }; @@ -2703,16 +2724,16 @@ internal sealed class CommanderPlanningService HomeStationId = objective.HomeStationId ?? fallback.HomeStationId, AreaSystemId = areaSystemId, TargetEntityId = objective.TargetEntityId, - PreferredItemId = objective.ItemId ?? fallback.PreferredItemId, + ItemId = objective.ItemId ?? fallback.ItemId, PreferredNodeId = fallback.PreferredNodeId, PreferredConstructionSiteId = objective.Kind is "construct-site" or "supply-site" ? objective.TargetEntityId : fallback.PreferredConstructionSiteId, PreferredModuleId = fallback.PreferredModuleId, TargetPosition = objective.TargetPosition ?? fallback.TargetPosition, - WaitSeconds = objective.BehaviorKind == "supply-fleet" ? 4f : fallback.WaitSeconds, + WaitSeconds = objective.BehaviorKind == SupplyFleet ? 4f : fallback.WaitSeconds, Radius = radius, MaxSystemRange = maxRange, - KnownStationsOnly = objective.BehaviorKind == "revisit-known-stations", - PatrolPoints = objective.BehaviorKind == "patrol" + KnownStationsOnly = objective.BehaviorKind == RevisitKnownStations, + PatrolPoints = objective.BehaviorKind == Patrol ? BuildPatrolPoints(objective.TargetPosition ?? fallback.TargetPosition ?? ship.Position, radius) : [], PatrolIndex = ship.DefaultBehavior.PatrolIndex, @@ -2728,7 +2749,7 @@ internal sealed class CommanderPlanningService target.HomeStationId = source.HomeStationId; target.AreaSystemId = source.AreaSystemId; target.TargetEntityId = source.TargetEntityId; - target.PreferredItemId = source.PreferredItemId; + target.ItemId = source.ItemId; target.PreferredNodeId = source.PreferredNodeId; target.PreferredConstructionSiteId = source.PreferredConstructionSiteId; target.PreferredModuleId = source.PreferredModuleId; @@ -2749,7 +2770,7 @@ internal sealed class CommanderPlanningService && string.Equals(left.HomeStationId, right.HomeStationId, StringComparison.Ordinal) && string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) - && string.Equals(left.PreferredItemId, right.PreferredItemId, StringComparison.Ordinal) + && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.PreferredNodeId, right.PreferredNodeId, StringComparison.Ordinal) && string.Equals(left.PreferredConstructionSiteId, right.PreferredConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal) @@ -2805,13 +2826,15 @@ internal sealed class CommanderPlanningService { Id = $"ai-order-{objective.Id}", Kind = objective.StagingOrderKind, + SourceKind = ShipOrderSourceKind.Commander, + SourceId = objective.Id, Priority = 90 + objective.ReinforcementLevel, InterruptCurrentPlan = true, Label = $"{objective.Kind} staging", TargetEntityId = objective.TargetEntityId, TargetSystemId = targetSystemId, TargetPosition = targetPosition, - DestinationStationId = objective.BehaviorKind == "dock-and-wait" ? objective.TargetEntityId : null, + DestinationStationId = objective.BehaviorKind == DockAndWait ? objective.TargetEntityId : null, ItemId = objective.ItemId, WaitSeconds = 0f, Radius = MathF.Max(12f, objective.ReinforcementLevel * 18f), @@ -2885,6 +2908,8 @@ internal sealed class CommanderPlanningService private static bool ShipOrdersEqual(ShipOrderRuntime left, ShipOrderRuntime right) => string.Equals(left.Id, right.Id, StringComparison.Ordinal) && string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) + && left.SourceKind == right.SourceKind + && string.Equals(left.SourceId, right.SourceId, StringComparison.Ordinal) && left.Priority == right.Priority && left.InterruptCurrentPlan == right.InterruptCurrentPlan && string.Equals(left.Label, right.Label, StringComparison.Ordinal) @@ -2920,7 +2945,7 @@ internal sealed class CommanderPlanningService } private static bool IsCombatObjective(FactionOperationalObjectiveRuntime objective) => - objective.BehaviorKind is "attack-target" or "protect-position" or "protect-ship" or "protect-station" or "patrol" or "police"; + objective.BehaviorKind is AttackTarget or ProtectPosition or ProtectShip or ProtectStation or Patrol or Police; private static float EstimateFriendlyAssetValue(SimulationWorld world, string factionId, string systemId) { diff --git a/apps/backend/Geopolitics/Simulation/GeopoliticalSimulationService.cs b/apps/backend/Geopolitics/Simulation/GeopoliticalSimulationService.cs index de0b3f7..5e4fe0a 100644 --- a/apps/backend/Geopolitics/Simulation/GeopoliticalSimulationService.cs +++ b/apps/backend/Geopolitics/Simulation/GeopoliticalSimulationService.cs @@ -1,5 +1,7 @@ using System.Globalization; +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; + namespace SpaceGame.Api.Geopolitics.Simulation; internal sealed class GeopoliticalSimulationService @@ -198,14 +200,24 @@ internal sealed class GeopoliticalSimulationService var stationStrength = (stationsByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 50f; var siteStrength = (sitesByFaction.FirstOrDefault(group => group.Key == factionId)?.Count() ?? 0) * 18f; var shipStrength = shipsByFaction.FirstOrDefault(group => group.Key == factionId)?.Sum(ship => - ship.Definition.Kind switch - { - "military" => 9f, - "construction" => 4f, - "transport" => 3f, - _ when ship.Definition.Kind == "mining" || ship.Definition.Kind == "miner" => 3f, - _ => 2f, - }) ?? 0f; + { + if (IsMilitaryShip(ship.Definition)) + { + return 9f; + } + + if (IsConstructionShip(ship.Definition)) + { + return 4f; + } + + if (IsTransportShip(ship.Definition) || IsMiningShip(ship.Definition)) + { + return 3f; + } + + return 2f; + }) ?? 0f; var logisticsStrength = MathF.Min(30f, stationStrength * 0.18f) + siteStrength; influences.Add(new TerritoryInfluenceRuntime { diff --git a/apps/backend/GlobalUsings.cs b/apps/backend/GlobalUsings.cs index ff76e34..6bcc762 100644 --- a/apps/backend/GlobalUsings.cs +++ b/apps/backend/GlobalUsings.cs @@ -1,3 +1,6 @@ +global using SpaceGame.Api.Auth.Contracts; +global using SpaceGame.Api.Auth.Runtime; +global using SpaceGame.Api.Auth.Simulation; global using SpaceGame.Api.Definitions; global using SpaceGame.Api.Economy.Contracts; global using SpaceGame.Api.Economy.Runtime; @@ -15,7 +18,7 @@ global using SpaceGame.Api.Shared.Contracts; global using SpaceGame.Api.Shared.Runtime; global using SpaceGame.Api.Ships.Contracts; global using SpaceGame.Api.Ships.Runtime; -global using SpaceGame.Api.Ships.Simulation; +global using SpaceGame.Api.Ships.AI; global using SpaceGame.Api.Simulation.Core; global using SpaceGame.Api.Stations.Contracts; global using SpaceGame.Api.Stations.Runtime; diff --git a/apps/backend/PlayerFaction/Api/CreatePlayerOrganizationHandler.cs b/apps/backend/PlayerFaction/Api/CreatePlayerOrganizationHandler.cs index 043dc09..444ce89 100644 --- a/apps/backend/PlayerFaction/Api/CreatePlayerOrganizationHandler.cs +++ b/apps/backend/PlayerFaction/Api/CreatePlayerOrganizationHandler.cs @@ -7,7 +7,6 @@ public sealed class CreatePlayerOrganizationHandler(WorldService worldService) : public override void Configure() { Post("/api/player-faction/organizations"); - AllowAnonymous(); } public override async Task HandleAsync(PlayerOrganizationCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/DeletePlayerDirectiveHandler.cs b/apps/backend/PlayerFaction/Api/DeletePlayerDirectiveHandler.cs index fe3626c..dced7ac 100644 --- a/apps/backend/PlayerFaction/Api/DeletePlayerDirectiveHandler.cs +++ b/apps/backend/PlayerFaction/Api/DeletePlayerDirectiveHandler.cs @@ -12,7 +12,6 @@ public sealed class DeletePlayerDirectiveHandler(WorldService worldService) : En public override void Configure() { Delete("/api/player-faction/directives/{directiveId}"); - AllowAnonymous(); } public override async Task HandleAsync(DeletePlayerDirectiveRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/DeletePlayerOrganizationHandler.cs b/apps/backend/PlayerFaction/Api/DeletePlayerOrganizationHandler.cs index d581c20..1dcc4a5 100644 --- a/apps/backend/PlayerFaction/Api/DeletePlayerOrganizationHandler.cs +++ b/apps/backend/PlayerFaction/Api/DeletePlayerOrganizationHandler.cs @@ -12,7 +12,6 @@ public sealed class DeletePlayerOrganizationHandler(WorldService worldService) : public override void Configure() { Delete("/api/player-faction/organizations/{organizationId}"); - AllowAnonymous(); } public override async Task HandleAsync(DeletePlayerOrganizationRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/GetPlayerFactionHandler.cs b/apps/backend/PlayerFaction/Api/GetPlayerFactionHandler.cs index 1bbc8be..909c835 100644 --- a/apps/backend/PlayerFaction/Api/GetPlayerFactionHandler.cs +++ b/apps/backend/PlayerFaction/Api/GetPlayerFactionHandler.cs @@ -7,7 +7,6 @@ public sealed class GetPlayerFactionHandler(WorldService worldService) : Endpoin public override void Configure() { Get("/api/player-faction"); - AllowAnonymous(); } public override async Task HandleAsync(CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/UpdatePlayerOrganizationMembershipHandler.cs b/apps/backend/PlayerFaction/Api/UpdatePlayerOrganizationMembershipHandler.cs index 29aa0b6..ce1bedd 100644 --- a/apps/backend/PlayerFaction/Api/UpdatePlayerOrganizationMembershipHandler.cs +++ b/apps/backend/PlayerFaction/Api/UpdatePlayerOrganizationMembershipHandler.cs @@ -7,7 +7,6 @@ public sealed class UpdatePlayerOrganizationMembershipHandler(WorldService world public override void Configure() { Put("/api/player-faction/organizations/{organizationId}/membership"); - AllowAnonymous(); } public override async Task HandleAsync(PlayerOrganizationMembershipCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/UpdatePlayerStrategicIntentHandler.cs b/apps/backend/PlayerFaction/Api/UpdatePlayerStrategicIntentHandler.cs index 05f509f..cb846a5 100644 --- a/apps/backend/PlayerFaction/Api/UpdatePlayerStrategicIntentHandler.cs +++ b/apps/backend/PlayerFaction/Api/UpdatePlayerStrategicIntentHandler.cs @@ -7,7 +7,6 @@ public sealed class UpdatePlayerStrategicIntentHandler(WorldService worldService public override void Configure() { Put("/api/player-faction/strategic-intent"); - AllowAnonymous(); } public override async Task HandleAsync(PlayerStrategicIntentCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/UpsertPlayerAssignmentHandler.cs b/apps/backend/PlayerFaction/Api/UpsertPlayerAssignmentHandler.cs index 0896571..cf99e9d 100644 --- a/apps/backend/PlayerFaction/Api/UpsertPlayerAssignmentHandler.cs +++ b/apps/backend/PlayerFaction/Api/UpsertPlayerAssignmentHandler.cs @@ -7,7 +7,6 @@ public sealed class UpsertPlayerAssignmentHandler(WorldService worldService) : E public override void Configure() { Put("/api/player-faction/assets/{assetId}/assignment"); - AllowAnonymous(); } public override async Task HandleAsync(PlayerAssetAssignmentCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/UpsertPlayerAutomationPolicyHandler.cs b/apps/backend/PlayerFaction/Api/UpsertPlayerAutomationPolicyHandler.cs index b0759f4..a3cc54b 100644 --- a/apps/backend/PlayerFaction/Api/UpsertPlayerAutomationPolicyHandler.cs +++ b/apps/backend/PlayerFaction/Api/UpsertPlayerAutomationPolicyHandler.cs @@ -8,7 +8,6 @@ public sealed class UpsertPlayerAutomationPolicyHandler(WorldService worldServic { Post("/api/player-faction/automation-policies"); Put("/api/player-faction/automation-policies/{automationPolicyId}"); - AllowAnonymous(); } public override async Task HandleAsync(PlayerAutomationPolicyCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/UpsertPlayerDirectiveHandler.cs b/apps/backend/PlayerFaction/Api/UpsertPlayerDirectiveHandler.cs index d622c83..ebdbbe7 100644 --- a/apps/backend/PlayerFaction/Api/UpsertPlayerDirectiveHandler.cs +++ b/apps/backend/PlayerFaction/Api/UpsertPlayerDirectiveHandler.cs @@ -8,7 +8,6 @@ public sealed class UpsertPlayerDirectiveHandler(WorldService worldService) : En { Post("/api/player-faction/directives"); Put("/api/player-faction/directives/{directiveId}"); - AllowAnonymous(); } public override async Task HandleAsync(PlayerDirectiveCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/UpsertPlayerPolicyHandler.cs b/apps/backend/PlayerFaction/Api/UpsertPlayerPolicyHandler.cs index f189aa7..10a36c7 100644 --- a/apps/backend/PlayerFaction/Api/UpsertPlayerPolicyHandler.cs +++ b/apps/backend/PlayerFaction/Api/UpsertPlayerPolicyHandler.cs @@ -8,7 +8,6 @@ public sealed class UpsertPlayerPolicyHandler(WorldService worldService) : Endpo { Post("/api/player-faction/policies"); Put("/api/player-faction/policies/{policyId}"); - AllowAnonymous(); } public override async Task HandleAsync(PlayerPolicyCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/UpsertPlayerProductionProgramHandler.cs b/apps/backend/PlayerFaction/Api/UpsertPlayerProductionProgramHandler.cs index 8d5fb36..67e61de 100644 --- a/apps/backend/PlayerFaction/Api/UpsertPlayerProductionProgramHandler.cs +++ b/apps/backend/PlayerFaction/Api/UpsertPlayerProductionProgramHandler.cs @@ -8,7 +8,6 @@ public sealed class UpsertPlayerProductionProgramHandler(WorldService worldServi { Post("/api/player-faction/production-programs"); Put("/api/player-faction/production-programs/{productionProgramId}"); - AllowAnonymous(); } public override async Task HandleAsync(PlayerProductionProgramCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Api/UpsertPlayerReinforcementPolicyHandler.cs b/apps/backend/PlayerFaction/Api/UpsertPlayerReinforcementPolicyHandler.cs index bc693fe..7adcebc 100644 --- a/apps/backend/PlayerFaction/Api/UpsertPlayerReinforcementPolicyHandler.cs +++ b/apps/backend/PlayerFaction/Api/UpsertPlayerReinforcementPolicyHandler.cs @@ -8,7 +8,6 @@ public sealed class UpsertPlayerReinforcementPolicyHandler(WorldService worldSer { Post("/api/player-faction/reinforcement-policies"); Put("/api/player-faction/reinforcement-policies/{reinforcementPolicyId}"); - AllowAnonymous(); } public override async Task HandleAsync(PlayerReinforcementPolicyCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/PlayerFaction/Runtime/PlayerFactionRuntimeModels.cs b/apps/backend/PlayerFaction/Runtime/PlayerFactionRuntimeModels.cs index 890e270..2edd146 100644 --- a/apps/backend/PlayerFaction/Runtime/PlayerFactionRuntimeModels.cs +++ b/apps/backend/PlayerFaction/Runtime/PlayerFactionRuntimeModels.cs @@ -1,3 +1,5 @@ +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; + namespace SpaceGame.Api.PlayerFaction.Runtime; public sealed class PlayerFactionRuntime @@ -180,7 +182,7 @@ public sealed class PlayerAutomationPolicyRuntime public string ScopeKind { get; set; } = "player-faction"; public string? ScopeId { get; set; } public bool Enabled { get; set; } = true; - public string BehaviorKind { get; set; } = "idle"; + public string BehaviorKind { get; set; } = Idle; public bool UseOrders { get; set; } public string? StagingOrderKind { get; set; } public int MaxSystemRange { get; set; } @@ -242,7 +244,7 @@ public sealed class PlayerDirectiveRuntime public string? HomeStationId { get; set; } public string? SourceStationId { get; set; } public string? DestinationStationId { get; set; } - public string BehaviorKind { get; set; } = "idle"; + public string BehaviorKind { get; set; } = Idle; public bool UseOrders { get; set; } public string? StagingOrderKind { get; set; } public string? ItemId { get; set; } diff --git a/apps/backend/PlayerFaction/Simulation/IPlayerStateStore.cs b/apps/backend/PlayerFaction/Simulation/IPlayerStateStore.cs new file mode 100644 index 0000000..56b3e45 --- /dev/null +++ b/apps/backend/PlayerFaction/Simulation/IPlayerStateStore.cs @@ -0,0 +1,9 @@ +namespace SpaceGame.Api.PlayerFaction.Simulation; + +public interface IPlayerStateStore +{ + bool TryGetPlayerFaction(string playerId, out PlayerFactionRuntime playerFaction); + PlayerFactionRuntime GetOrAddPlayerFaction(string playerId, Func factory); + IReadOnlyCollection GetPlayerFactions(); + void Clear(); +} diff --git a/apps/backend/PlayerFaction/Simulation/PlayerFactionProjectionService.cs b/apps/backend/PlayerFaction/Simulation/PlayerFactionProjectionService.cs new file mode 100644 index 0000000..fb5ac88 --- /dev/null +++ b/apps/backend/PlayerFaction/Simulation/PlayerFactionProjectionService.cs @@ -0,0 +1,270 @@ +namespace SpaceGame.Api.PlayerFaction.Simulation; + +public sealed class PlayerFactionProjectionService +{ + public PlayerFactionSnapshot? ToSnapshot(PlayerFactionRuntime? player) + { + if (player is null) + { + return null; + } + + return new PlayerFactionSnapshot( + player.Id, + player.Label, + player.SovereignFactionId, + player.Status, + player.CreatedAtUtc, + player.UpdatedAtUtc, + new PlayerAssetRegistrySnapshot( + player.AssetRegistry.ShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.StationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.CommanderIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.ClaimIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.ConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.PolicySetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.FleetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.TaskForceIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.StationGroupIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.EconomicRegionIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.FrontIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + player.AssetRegistry.ReserveIds.OrderBy(id => id, StringComparer.Ordinal).ToList()), + new PlayerStrategicIntentSnapshot( + player.StrategicIntent.StrategicPosture, + player.StrategicIntent.EconomicPosture, + player.StrategicIntent.MilitaryPosture, + player.StrategicIntent.LogisticsPosture, + player.StrategicIntent.DesiredReserveRatio, + player.StrategicIntent.AllowDelegatedCombatAutomation, + player.StrategicIntent.AllowDelegatedEconomicAutomation, + player.StrategicIntent.Notes), + player.Fleets.Select(fleet => new PlayerFleetSnapshot( + fleet.Id, + fleet.Label, + fleet.Status, + fleet.Role, + fleet.CommanderId, + fleet.FrontId, + fleet.HomeSystemId, + fleet.HomeStationId, + fleet.PolicyId, + fleet.AutomationPolicyId, + fleet.ReinforcementPolicyId, + fleet.AssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + fleet.TaskForceIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + fleet.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + fleet.UpdatedAtUtc)).ToList(), + player.TaskForces.Select(taskForce => new PlayerTaskForceSnapshot( + taskForce.Id, + taskForce.Label, + taskForce.Status, + taskForce.Role, + taskForce.FleetId, + taskForce.CommanderId, + taskForce.FrontId, + taskForce.PolicyId, + taskForce.AutomationPolicyId, + taskForce.AssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + taskForce.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + taskForce.UpdatedAtUtc)).ToList(), + player.StationGroups.Select(group => new PlayerStationGroupSnapshot( + group.Id, + group.Label, + group.Status, + group.Role, + group.EconomicRegionId, + group.PolicyId, + group.AutomationPolicyId, + group.StationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + group.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + group.FocusItemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + group.UpdatedAtUtc)).ToList(), + player.EconomicRegions.Select(region => new PlayerEconomicRegionSnapshot( + region.Id, + region.Label, + region.Status, + region.Role, + region.SharedEconomicRegionId, + region.PolicyId, + region.AutomationPolicyId, + region.SystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + region.StationGroupIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + region.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + region.UpdatedAtUtc)).ToList(), + player.Fronts.Select(front => new PlayerFrontSnapshot( + front.Id, + front.Label, + front.Status, + front.Priority, + front.Posture, + front.SharedFrontLineId, + front.TargetFactionId, + front.SystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + front.FleetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + front.ReserveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + front.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + front.UpdatedAtUtc)).ToList(), + player.Reserves.Select(reserve => new PlayerReserveGroupSnapshot( + reserve.Id, + reserve.Label, + reserve.Status, + reserve.ReserveKind, + reserve.HomeSystemId, + reserve.PolicyId, + reserve.AssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + reserve.FrontIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + reserve.UpdatedAtUtc)).ToList(), + player.Policies.Select(policy => new PlayerFactionPolicySnapshot( + policy.Id, + policy.Label, + policy.ScopeKind, + policy.ScopeId, + policy.PolicySetId, + policy.AllowDelegatedCombat, + policy.AllowDelegatedTrade, + policy.ReserveCreditsRatio, + policy.ReserveMilitaryRatio, + policy.TradeAccessPolicy, + policy.DockingAccessPolicy, + policy.ConstructionAccessPolicy, + policy.OperationalRangePolicy, + policy.CombatEngagementPolicy, + policy.AvoidHostileSystems, + policy.FleeHullRatio, + policy.BlacklistedSystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), + policy.Notes, + policy.UpdatedAtUtc)).ToList(), + player.AutomationPolicies.Select(policy => new PlayerAutomationPolicySnapshot( + policy.Id, + policy.Label, + policy.ScopeKind, + policy.ScopeId, + policy.Enabled, + policy.BehaviorKind, + policy.UseOrders, + policy.StagingOrderKind, + policy.MaxSystemRange, + policy.KnownStationsOnly, + policy.Radius, + policy.WaitSeconds, + policy.PreferredItemId, + policy.Notes, + policy.RepeatOrders.Select(ToShipOrderTemplateSnapshot).ToList(), + policy.UpdatedAtUtc)).ToList(), + player.ReinforcementPolicies.Select(policy => new PlayerReinforcementPolicySnapshot( + policy.Id, + policy.Label, + policy.ScopeKind, + policy.ScopeId, + policy.ShipKind, + policy.DesiredAssetCount, + policy.MinimumReserveCount, + policy.AutoTransferReserves, + policy.AutoQueueProduction, + policy.SourceReserveId, + policy.TargetFrontId, + policy.Notes, + policy.UpdatedAtUtc)).ToList(), + player.ProductionPrograms.Select(program => new PlayerProductionProgramSnapshot( + program.Id, + program.Label, + program.Status, + program.Kind, + program.TargetShipKind, + program.TargetModuleId, + program.TargetItemId, + program.TargetCount, + program.CurrentCount, + program.StationGroupId, + program.ReinforcementPolicyId, + program.Notes, + program.UpdatedAtUtc)).ToList(), + player.Directives.Select(directive => new PlayerDirectiveSnapshot( + directive.Id, + directive.Label, + directive.Status, + directive.Kind, + directive.ScopeKind, + directive.ScopeId, + directive.TargetEntityId, + directive.TargetSystemId, + directive.TargetPosition is null ? null : ToDto(directive.TargetPosition.Value), + directive.HomeSystemId, + directive.HomeStationId, + directive.SourceStationId, + directive.DestinationStationId, + directive.BehaviorKind, + directive.UseOrders, + directive.StagingOrderKind, + directive.ItemId, + directive.PreferredNodeId, + directive.PreferredConstructionSiteId, + directive.PreferredModuleId, + directive.Priority, + directive.Radius, + directive.WaitSeconds, + directive.MaxSystemRange, + directive.KnownStationsOnly, + directive.PatrolPoints.Select(ToDto).ToList(), + directive.RepeatOrders.Select(ToShipOrderTemplateSnapshot).ToList(), + directive.PolicyId, + directive.AutomationPolicyId, + directive.Notes, + directive.CreatedAtUtc, + directive.UpdatedAtUtc)).ToList(), + player.Assignments.Select(assignment => new PlayerAssignmentSnapshot( + assignment.Id, + assignment.AssetKind, + assignment.AssetId, + assignment.FleetId, + assignment.TaskForceId, + assignment.StationGroupId, + assignment.EconomicRegionId, + assignment.FrontId, + assignment.ReserveId, + assignment.DirectiveId, + assignment.PolicyId, + assignment.AutomationPolicyId, + assignment.Role, + assignment.Status, + assignment.UpdatedAtUtc)).ToList(), + player.DecisionLog.Select(entry => new PlayerDecisionLogEntrySnapshot( + entry.Id, + entry.Kind, + entry.Summary, + entry.RelatedEntityKind, + entry.RelatedEntityId, + entry.OccurredAtUtc)).ToList(), + player.Alerts.Select(alert => new PlayerAlertSnapshot( + alert.Id, + alert.Kind, + alert.Severity, + alert.Summary, + alert.AssetKind, + alert.AssetId, + alert.RelatedDirectiveId, + alert.Status, + alert.CreatedAtUtc)).ToList()); + } + + private static ShipOrderTemplateSnapshot ToShipOrderTemplateSnapshot(ShipOrderTemplateRuntime template) => + new( + template.Kind, + template.Label, + template.TargetEntityId, + template.TargetSystemId, + template.TargetPosition is null ? null : ToDto(template.TargetPosition.Value), + template.SourceStationId, + template.DestinationStationId, + template.ItemId, + template.NodeId, + template.ConstructionSiteId, + template.ModuleId, + template.WaitSeconds, + template.Radius, + template.MaxSystemRange, + template.KnownStationsOnly); + + private static Vector3Dto ToDto(Vector3 vector) => new(vector.X, vector.Y, vector.Z); +} diff --git a/apps/backend/PlayerFaction/Simulation/PlayerFactionService.cs b/apps/backend/PlayerFaction/Simulation/PlayerFactionService.cs index 56b54ab..7b96ab1 100644 --- a/apps/backend/PlayerFaction/Simulation/PlayerFactionService.cs +++ b/apps/backend/PlayerFaction/Simulation/PlayerFactionService.cs @@ -1,3 +1,6 @@ +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; + namespace SpaceGame.Api.PlayerFaction.Simulation; internal sealed class PlayerFactionService @@ -6,58 +9,61 @@ internal sealed class PlayerFactionService private const int MaxAlerts = 32; private const string PlayerFactionDomainId = "player-faction"; - internal static bool IsPlayerFaction(SimulationWorld world, string factionId) => - world.PlayerFaction is not null && string.Equals(world.PlayerFaction.SovereignFactionId, factionId, StringComparison.Ordinal); + internal static bool IsPlayerFaction(IPlayerStateStore playerStateStore, string factionId) => + playerStateStore.GetPlayerFactions().Any(player => + string.Equals(player.SovereignFactionId, factionId, StringComparison.Ordinal)); - internal PlayerFactionRuntime EnsureDomain(SimulationWorld world) + internal PlayerFactionRuntime? TryGetDomain(IPlayerStateStore playerStateStore, string playerId) { - if (world.PlayerFaction is not null) - { - return world.PlayerFaction; - } + return playerStateStore.TryGetPlayerFaction(playerId, out var player) ? player : null; + } + internal PlayerFactionRuntime EnsureDomain(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId) + { var sovereignFaction = world.Factions.OrderBy(faction => faction.Id, StringComparer.Ordinal).FirstOrDefault() ?? throw new InvalidOperationException("Cannot create a player faction domain without any factions in the world."); - world.PlayerFaction = new PlayerFactionRuntime + var player = playerStateStore.GetOrAddPlayerFaction(playerId, () => new PlayerFactionRuntime { Id = PlayerFactionDomainId, Label = $"{sovereignFaction.Label} Command", SovereignFactionId = sovereignFaction.Id, CreatedAtUtc = world.GeneratedAtUtc, UpdatedAtUtc = world.GeneratedAtUtc, - }; + }); - EnsureBaseStructures(world, world.PlayerFaction); - SyncRegistry(world, world.PlayerFaction); - return world.PlayerFaction; + EnsureBaseStructures(world, player); + SyncRegistry(world, player); + return player; } - internal void Update(SimulationWorld world, float _deltaSeconds, ICollection events) + internal void Update(SimulationWorld world, IPlayerStateStore playerStateStore, float _deltaSeconds, ICollection events) { - if (world.PlayerFaction is null && world.Factions.Count == 0) + if (playerStateStore.GetPlayerFactions().Count == 0) { return; } - var player = EnsureDomain(world); - EnsureBaseStructures(world, player); - SyncRegistry(world, player); - PrunePlayerState(world, player); - RefreshGeopoliticalOrganizationContext(world, player); - ReconcileOrganizationAssignments(world, player); - ReconcileDirectiveScopes(player); - RefreshProductionPrograms(world, player); - ApplyStrategicIntegration(world, player); - ApplyPolicies(world, player); - ApplyAssignmentsAndDirectives(world, player, events); - RefreshAlerts(world, player); - player.UpdatedAtUtc = DateTimeOffset.UtcNow; + foreach (var player in playerStateStore.GetPlayerFactions()) + { + EnsureBaseStructures(world, player); + SyncRegistry(world, player); + PrunePlayerState(world, player); + RefreshGeopoliticalOrganizationContext(world, player); + ReconcileOrganizationAssignments(world, player); + ReconcileDirectiveScopes(player); + RefreshProductionPrograms(world, player); + ApplyStrategicIntegration(world, player); + ApplyPolicies(world, player); + ApplyAssignmentsAndDirectives(world, player, events); + RefreshAlerts(world, player); + player.UpdatedAtUtc = DateTimeOffset.UtcNow; + } } - internal PlayerFactionRuntime CreateOrganization(SimulationWorld world, PlayerOrganizationCommandRequest request) + internal PlayerFactionRuntime CreateOrganization(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, PlayerOrganizationCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); var id = CreateDomainId(request.Kind, request.Label, ExistingOrganizationIds(player)); var nowUtc = DateTimeOffset.UtcNow; @@ -172,9 +178,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime DeleteOrganization(SimulationWorld world, string organizationId) + internal PlayerFactionRuntime DeleteOrganization(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string organizationId) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); RemoveOrganization(player, organizationId); player.Assignments.RemoveAll(assignment => assignment.FleetId == organizationId || @@ -190,9 +196,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime UpdateOrganizationMembership(SimulationWorld world, string organizationId, PlayerOrganizationMembershipCommandRequest request) + internal PlayerFactionRuntime UpdateOrganizationMembership(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string organizationId, PlayerOrganizationMembershipCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); var kind = ResolveOrganizationKind(player, organizationId); switch (kind) { @@ -241,9 +247,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime UpsertDirective(SimulationWorld world, string? directiveId, PlayerDirectiveCommandRequest request) + internal PlayerFactionRuntime UpsertDirective(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? directiveId, PlayerDirectiveCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); var directive = directiveId is null ? null : player.Directives.FirstOrDefault(candidate => string.Equals(candidate.Id, directiveId, StringComparison.Ordinal)); @@ -318,9 +324,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime DeleteDirective(SimulationWorld world, string directiveId) + internal PlayerFactionRuntime DeleteDirective(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string directiveId) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); player.Directives.RemoveAll(directive => directive.Id == directiveId); foreach (var assignment in player.Assignments.Where(assignment => assignment.DirectiveId == directiveId)) { @@ -332,9 +338,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime UpsertPolicy(SimulationWorld world, string? policyId, PlayerPolicyCommandRequest request) + internal PlayerFactionRuntime UpsertPolicy(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? policyId, PlayerPolicyCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); var policy = policyId is null ? null : player.Policies.FirstOrDefault(candidate => string.Equals(candidate.Id, policyId, StringComparison.Ordinal)); @@ -403,9 +409,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime UpsertAutomationPolicy(SimulationWorld world, string? automationPolicyId, PlayerAutomationPolicyCommandRequest request) + internal PlayerFactionRuntime UpsertAutomationPolicy(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? automationPolicyId, PlayerAutomationPolicyCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); var policy = automationPolicyId is null ? null : player.AutomationPolicies.FirstOrDefault(candidate => string.Equals(candidate.Id, automationPolicyId, StringComparison.Ordinal)); @@ -461,9 +467,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime UpsertReinforcementPolicy(SimulationWorld world, string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request) + internal PlayerFactionRuntime UpsertReinforcementPolicy(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? reinforcementPolicyId, PlayerReinforcementPolicyCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); var policy = reinforcementPolicyId is null ? null : player.ReinforcementPolicies.FirstOrDefault(candidate => string.Equals(candidate.Id, reinforcementPolicyId, StringComparison.Ordinal)); @@ -495,9 +501,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime UpsertProductionProgram(SimulationWorld world, string? productionProgramId, PlayerProductionProgramCommandRequest request) + internal PlayerFactionRuntime UpsertProductionProgram(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string? productionProgramId, PlayerProductionProgramCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); var program = productionProgramId is null ? null : player.ProductionPrograms.FirstOrDefault(candidate => string.Equals(candidate.Id, productionProgramId, StringComparison.Ordinal)); @@ -527,9 +533,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime UpsertAssignment(SimulationWorld world, string assetId, PlayerAssetAssignmentCommandRequest request) + internal PlayerFactionRuntime UpsertAssignment(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string assetId, PlayerAssetAssignmentCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); var assignment = player.Assignments.FirstOrDefault(candidate => string.Equals(candidate.AssetId, assetId, StringComparison.Ordinal) && string.Equals(candidate.AssetKind, request.AssetKind, StringComparison.Ordinal)); @@ -586,9 +592,9 @@ internal sealed class PlayerFactionService return player; } - internal PlayerFactionRuntime UpdateStrategicIntent(SimulationWorld world, PlayerStrategicIntentCommandRequest request) + internal PlayerFactionRuntime UpdateStrategicIntent(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, PlayerStrategicIntentCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); player.StrategicIntent.StrategicPosture = request.StrategicPosture; player.StrategicIntent.EconomicPosture = request.EconomicPosture; player.StrategicIntent.MilitaryPosture = request.MilitaryPosture; @@ -602,9 +608,9 @@ internal sealed class PlayerFactionService return player; } - internal ShipRuntime? EnqueueDirectShipOrder(SimulationWorld world, string shipId, ShipOrderCommandRequest request) + internal ShipRuntime? EnqueueDirectShipOrder(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string shipId, ShipOrderCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); if (!player.AssetRegistry.ShipIds.Contains(shipId)) { return null; @@ -625,6 +631,8 @@ internal sealed class PlayerFactionService { Id = $"order-{ship.Id}-{Guid.NewGuid():N}", Kind = request.Kind, + SourceKind = ShipOrderSourceKind.Player, + SourceId = playerId, Priority = request.Priority, InterruptCurrentPlan = request.InterruptCurrentPlan, Label = request.Label, @@ -643,11 +651,11 @@ internal sealed class PlayerFactionService KnownStationsOnly = request.KnownStationsOnly ?? false, }); - AddDecision(player, "ship-order-enqueued", $"Queued {request.Kind} for {ship.Definition.Label}.", "ship", shipId); + AddDecision(player, "ship-order-enqueued", $"Queued {request.Kind} for {ship.Definition.Name}.", "ship", shipId); player.UpdatedAtUtc = DateTimeOffset.UtcNow; ship.ControlSourceKind = "player-order"; ship.ControlSourceId = ship.OrderQueue - .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) + .Where(order => order.SourceKind == ShipOrderSourceKind.Player) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Id) @@ -659,9 +667,9 @@ internal sealed class PlayerFactionService return ship; } - internal ShipRuntime? RemoveDirectShipOrder(SimulationWorld world, string shipId, string orderId) + internal ShipRuntime? RemoveDirectShipOrder(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string shipId, string orderId) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); if (!player.AssetRegistry.ShipIds.Contains(shipId)) { return null; @@ -676,21 +684,21 @@ internal sealed class PlayerFactionService var removed = ship.OrderQueue.RemoveAll(order => order.Id == orderId); if (removed > 0) { - AddDecision(player, "ship-order-removed", $"Removed order {orderId} from {ship.Definition.Label}.", "ship", shipId); + AddDecision(player, "ship-order-removed", $"Removed order {orderId} from {ship.Definition.Name}.", "ship", shipId); player.UpdatedAtUtc = DateTimeOffset.UtcNow; } - ship.ControlSourceKind = ship.OrderQueue.Any(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) + ship.ControlSourceKind = ship.OrderQueue.Any(order => order.SourceKind == ShipOrderSourceKind.Player) ? "player-order" : "player-manual"; ship.ControlSourceId = ship.OrderQueue - .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) + .Where(order => order.SourceKind == ShipOrderSourceKind.Player) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Id) .FirstOrDefault(); ship.ControlReason = ship.OrderQueue - .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) + .Where(order => order.SourceKind == ShipOrderSourceKind.Player) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Label ?? order.Kind) @@ -702,9 +710,9 @@ internal sealed class PlayerFactionService return ship; } - internal ShipRuntime? ConfigureDirectShipBehavior(SimulationWorld world, string shipId, ShipDefaultBehaviorCommandRequest request) + internal ShipRuntime? ConfigureDirectShipBehavior(SimulationWorld world, IPlayerStateStore playerStateStore, string playerId, string shipId, ShipDefaultBehaviorCommandRequest request) { - var player = EnsureDomain(world); + var player = EnsureDomain(world, playerStateStore, playerId); if (!player.AssetRegistry.ShipIds.Contains(shipId)) { return null; @@ -723,7 +731,7 @@ internal sealed class PlayerFactionService directive = new PlayerDirectiveRuntime { Id = directiveId, - Label = $"Direct control {ship.Definition.Label}", + Label = $"Direct control {ship.Definition.Name}", ScopeKind = "ship", ScopeId = shipId, Kind = "direct-control", @@ -732,7 +740,7 @@ internal sealed class PlayerFactionService player.Directives.Add(directive); } - directive.Label = $"Direct control {ship.Definition.Label}"; + directive.Label = $"Direct control {ship.Definition.Name}"; directive.Kind = "direct-control"; directive.ScopeKind = "ship"; directive.ScopeId = shipId; @@ -746,7 +754,7 @@ internal sealed class PlayerFactionService directive.HomeStationId = request.HomeStationId; directive.SourceStationId = request.HomeStationId; directive.DestinationStationId = null; - directive.ItemId = request.PreferredItemId; + directive.ItemId = request.ItemId; directive.PreferredNodeId = request.PreferredNodeId; directive.PreferredConstructionSiteId = request.PreferredConstructionSiteId; directive.PreferredModuleId = request.PreferredModuleId; @@ -793,7 +801,7 @@ internal sealed class PlayerFactionService ship.ControlSourceKind = "player-directive"; ship.ControlSourceId = directive.Id; ship.ControlReason = directive.Label; - AddDecision(player, "ship-behavior-configured", $"Configured {request.Kind} for {ship.Definition.Label}.", "ship", shipId); + AddDecision(player, "ship-behavior-configured", $"Configured {request.Kind} for {ship.Definition.Name}.", "ship", shipId); player.UpdatedAtUtc = directive.UpdatedAtUtc; ship.NeedsReplan = true; ship.LastReplanReason = "player-behavior-configured"; @@ -826,7 +834,7 @@ internal sealed class PlayerFactionService { Id = "player-core-automation", Label = "Core Automation", - BehaviorKind = "idle", + BehaviorKind = Idle, }); } @@ -1035,7 +1043,7 @@ internal sealed class PlayerFactionService var changed = ApplyDirectiveToShip(commander, ship, directive, automation, assignment); if (changed && directive is not null) { - events.Add(new SimulationEventRecord("ship", ship.Id, "player-directive", $"{ship.Definition.Label} aligned to player directive {directive.Label}.", DateTimeOffset.UtcNow, "player", "universe", ship.Id)); + events.Add(new SimulationEventRecord("ship", ship.Id, "player-directive", $"{ship.Definition.Name} aligned to player directive {directive.Label}.", DateTimeOffset.UtcNow, "player", "universe", ship.Id)); } } @@ -1246,13 +1254,13 @@ internal sealed class PlayerFactionService ? "player-directive" : automation is not null ? "player-automation" - : ship.OrderQueue.Any(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) + : ship.OrderQueue.Any(order => order.SourceKind == ShipOrderSourceKind.Player) ? "player-order" : "player-manual"; var desiredControlSourceId = directive?.Id ?? automation?.Id ?? ship.OrderQueue - .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) + .Where(order => order.SourceKind == ShipOrderSourceKind.Player) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Id) @@ -1260,7 +1268,7 @@ internal sealed class PlayerFactionService var desiredControlReason = directive?.Label ?? automation?.Label ?? ship.OrderQueue - .Where(order => !order.Id.StartsWith("ai-order-", StringComparison.Ordinal)) + .Where(order => order.SourceKind == ShipOrderSourceKind.Player) .OrderByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => order.Label ?? order.Kind) @@ -1342,7 +1350,7 @@ internal sealed class PlayerFactionService HomeStationId = directive?.HomeStationId ?? ship.DefaultBehavior.HomeStationId, AreaSystemId = directive?.TargetSystemId ?? directive?.HomeSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId, TargetEntityId = directive?.TargetEntityId, - PreferredItemId = directive?.ItemId ?? automation?.PreferredItemId ?? ship.DefaultBehavior.PreferredItemId, + ItemId = directive?.ItemId ?? automation?.PreferredItemId ?? ship.DefaultBehavior.ItemId, PreferredNodeId = directive?.PreferredNodeId ?? ship.DefaultBehavior.PreferredNodeId, PreferredConstructionSiteId = directive?.PreferredConstructionSiteId ?? ship.DefaultBehavior.PreferredConstructionSiteId, PreferredModuleId = directive?.PreferredModuleId ?? ship.DefaultBehavior.PreferredModuleId, @@ -1375,6 +1383,8 @@ internal sealed class PlayerFactionService { Id = aiOrderId!, Kind = directive.StagingOrderKind!, + SourceKind = ShipOrderSourceKind.Player, + SourceId = directive.Id, Priority = Math.Max(0, directive.Priority), InterruptCurrentPlan = true, Label = directive.Label, @@ -1447,7 +1457,7 @@ internal sealed class PlayerFactionService target.HomeStationId = source.HomeStationId; target.AreaSystemId = source.AreaSystemId; target.TargetEntityId = source.TargetEntityId; - target.PreferredItemId = source.PreferredItemId; + target.ItemId = source.ItemId; target.PreferredNodeId = source.PreferredNodeId; target.PreferredConstructionSiteId = source.PreferredConstructionSiteId; target.PreferredModuleId = source.PreferredModuleId; @@ -1468,7 +1478,7 @@ internal sealed class PlayerFactionService && string.Equals(left.HomeStationId, right.HomeStationId, StringComparison.Ordinal) && string.Equals(left.AreaSystemId, right.AreaSystemId, StringComparison.Ordinal) && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) - && string.Equals(left.PreferredItemId, right.PreferredItemId, StringComparison.Ordinal) + && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) && string.Equals(left.PreferredNodeId, right.PreferredNodeId, StringComparison.Ordinal) && string.Equals(left.PreferredConstructionSiteId, right.PreferredConstructionSiteId, StringComparison.Ordinal) && string.Equals(left.PreferredModuleId, right.PreferredModuleId, StringComparison.Ordinal) @@ -1501,6 +1511,8 @@ internal sealed class PlayerFactionService private static bool ShipOrdersEqual(ShipOrderRuntime left, ShipOrderRuntime right) => string.Equals(left.Id, right.Id, StringComparison.Ordinal) && string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) + && left.SourceKind == right.SourceKind + && string.Equals(left.SourceId, right.SourceId, StringComparison.Ordinal) && left.Priority == right.Priority && left.InterruptCurrentPlan == right.InterruptCurrentPlan && string.Equals(left.Label, right.Label, StringComparison.Ordinal) @@ -1716,7 +1728,7 @@ internal sealed class PlayerFactionService { program.CurrentCount = world.Ships.Count(ship => ship.FactionId == player.SovereignFactionId && - string.Equals(ship.Definition.Kind, program.TargetShipKind, StringComparison.Ordinal)); + string.Equals(GetShipCategory(ship.Definition), program.TargetShipKind, StringComparison.Ordinal)); } else { @@ -2113,7 +2125,7 @@ internal sealed class PlayerFactionService { var available = world.Ships.Count(ship => ship.FactionId == player.SovereignFactionId && - string.Equals(ship.Definition.Kind, policy.ShipKind, StringComparison.Ordinal)); + string.Equals(GetShipCategory(ship.Definition), policy.ShipKind, StringComparison.Ordinal)); if (available < policy.DesiredAssetCount) { player.Alerts.Add(new PlayerAlertRuntime diff --git a/apps/backend/PlayerFaction/Simulation/PlayerStateStore.cs b/apps/backend/PlayerFaction/Simulation/PlayerStateStore.cs new file mode 100644 index 0000000..3f6c2ee --- /dev/null +++ b/apps/backend/PlayerFaction/Simulation/PlayerStateStore.cs @@ -0,0 +1,26 @@ +namespace SpaceGame.Api.PlayerFaction.Simulation; + +public sealed class PlayerStateStore : IPlayerStateStore +{ + private readonly Dictionary _playerFactions = new(StringComparer.Ordinal); + + public bool TryGetPlayerFaction(string playerId, out PlayerFactionRuntime playerFaction) => + _playerFactions.TryGetValue(playerId, out playerFaction!); + + public PlayerFactionRuntime GetOrAddPlayerFaction(string playerId, Func factory) + { + if (_playerFactions.TryGetValue(playerId, out var existing)) + { + return existing; + } + + var created = factory(); + _playerFactions[playerId] = created; + return created; + } + + public IReadOnlyCollection GetPlayerFactions() => + _playerFactions.Values.ToList(); + + public void Clear() => _playerFactions.Clear(); +} diff --git a/apps/backend/Program.cs b/apps/backend/Program.cs index 1d7e2ea..8bbc951 100644 --- a/apps/backend/Program.cs +++ b/apps/backend/Program.cs @@ -1,11 +1,14 @@ +using System.Text; using FastEndpoints; using FastEndpoints.Swagger; -using SpaceGame.Api.Universe.Scenario; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Npgsql; using SpaceGame.Api.Universe.Bootstrap; -using SpaceGame.Api.Universe.Simulation; + +const string StartupScenarioPath = "scenarios/empty.json"; var builder = WebApplication.CreateBuilder(args); -const string StartupScenarioPath = "scenarios/empty.json"; builder.Services.AddCors((options) => { @@ -46,10 +49,67 @@ builder.Services }) .ValidateOnStart(); builder.Services.Configure(builder.Configuration.GetSection("Balance")); -builder.Services.Configure(builder.Configuration.GetSection("WorldGeneration")); builder.Services.Configure(builder.Configuration.GetSection("OrbitalSimulation")); +builder.Services + .AddOptions() + .Bind(builder.Configuration.GetSection("Auth")) + .Validate(options => !string.IsNullOrWhiteSpace(options.ConnectionString), "Auth:ConnectionString must be configured.") + .ValidateOnStart(); +builder.Services + .AddOptions() + .Bind(builder.Configuration.GetSection("Jwt")) + .Validate(options => !string.IsNullOrWhiteSpace(options.SigningKey), "Jwt:SigningKey must be configured.") + .ValidateOnStart(); + +var jwtOptions = builder.Configuration.GetSection("Jwt").Get() ?? new JwtOptions(); +var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateIssuerSigningKey = true, + ValidateLifetime = true, + ValidIssuer = jwtOptions.Issuer, + ValidAudience = jwtOptions.Audience, + IssuerSigningKey = signingKey, + ClockSkew = TimeSpan.FromSeconds(30), + }; + }); +builder.Services + .AddAuthorizationBuilder() + .AddPolicy(AuthPolicyNames.AdminAccess, policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireRole(AuthRoleNames.Admin); + }) + .AddPolicy(AuthPolicyNames.GmAccess, policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireRole(AuthRoleNames.Gm); + }); +builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton((serviceProvider) => +{ + var authOptions = serviceProvider.GetRequiredService>(); + return new NpgsqlDataSourceBuilder(authOptions.Value.ConnectionString).Build(); +}); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); @@ -68,9 +128,16 @@ builder.Services.AddFastEndpoints(); builder.Services.SwaggerDocument(); var app = builder.Build(); -app.Services.GetRequiredService().LoadFromScenario(StartupScenarioPath); +await app.Services.GetRequiredService().EnsureSchemaAsync(CancellationToken.None); +if (builder.Environment.IsDevelopment()) +{ + await app.Services.GetRequiredService().SeedAsync(CancellationToken.None); + app.Services.GetRequiredService().LoadFromScenario(StartupScenarioPath); +} app.UseCors(); +app.UseAuthentication(); +app.UseAuthorization(); app.UseFastEndpoints(); app.UseSwaggerGen(); diff --git a/apps/backend/Shared/Contracts/VersionInfo.cs b/apps/backend/Shared/Contracts/VersionInfo.cs new file mode 100644 index 0000000..2b2767b --- /dev/null +++ b/apps/backend/Shared/Contracts/VersionInfo.cs @@ -0,0 +1,7 @@ +namespace SpaceGame.Api.Shared.Contracts; + +public sealed record VersionInfoSnapshot( + string Version, + string Environment, + string? CommitSha, + DateTimeOffset StartedAtUtc); diff --git a/apps/backend/Shared/Runtime/AppVersionService.cs b/apps/backend/Shared/Runtime/AppVersionService.cs new file mode 100644 index 0000000..02d0e7d --- /dev/null +++ b/apps/backend/Shared/Runtime/AppVersionService.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using Microsoft.Extensions.Hosting; + +namespace SpaceGame.Api.Shared.Runtime; + +public sealed class AppVersionService +{ + private readonly VersionInfoSnapshot _snapshot; + + public AppVersionService(IHostEnvironment environment) + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + var informationalVersion = assembly + .GetCustomAttribute()? + .InformationalVersion; + var assemblyVersion = assembly.GetName().Version?.ToString() ?? "0.0.0"; + var version = string.IsNullOrWhiteSpace(informationalVersion) ? assemblyVersion : informationalVersion; + var commitSha = Environment.GetEnvironmentVariable("SPACEGAME_COMMIT_SHA") + ?? Environment.GetEnvironmentVariable("GIT_COMMIT_SHA"); + + _snapshot = new VersionInfoSnapshot( + version, + environment.EnvironmentName, + string.IsNullOrWhiteSpace(commitSha) ? null : commitSha, + DateTimeOffset.UtcNow); + } + + public VersionInfoSnapshot GetSnapshot() => _snapshot; +} diff --git a/apps/backend/Shared/Runtime/KnownShipTaxonomy.cs b/apps/backend/Shared/Runtime/KnownShipTaxonomy.cs new file mode 100644 index 0000000..cb1492a --- /dev/null +++ b/apps/backend/Shared/Runtime/KnownShipTaxonomy.cs @@ -0,0 +1,69 @@ +namespace SpaceGame.Api.Shared.Runtime; + +internal static class KnownShipTypes +{ + internal const string Resupplier = "resupplier"; + internal const string Miner = "miner"; + internal const string Carrier = "carrier"; + internal const string Fighter = "fighter"; + internal const string HeavyFighter = "heavyfighter"; + internal const string Destroyer = "destroyer"; + internal const string LargeMiner = "largeminer"; + internal const string Freighter = "freighter"; + internal const string Bomber = "bomber"; + internal const string Scavenger = "scavenger"; + internal const string Frigate = "frigate"; + internal const string Transporter = "transporter"; + internal const string Interceptor = "interceptor"; + internal const string Scout = "scout"; + internal const string Courier = "courier"; + internal const string Builder = "builder"; + internal const string Corvette = "corvette"; + internal const string Police = "police"; + internal const string Battleship = "battleship"; + internal const string Gunboat = "gunboat"; + internal const string Tug = "tug"; + internal const string Compactor = "compactor"; +} + +internal static class ShipTaxonomyExtensions +{ + internal static string ToDataValue(this ShipPurpose purpose) => + purpose switch + { + ShipPurpose.Auxiliary => "auxiliary", + ShipPurpose.Build => "build", + ShipPurpose.Fight => "fight", + ShipPurpose.Mine => "mine", + ShipPurpose.Trade => "trade", + _ => purpose.ToString(), + }; + + internal static string ToDataValue(this ShipType type) => + type switch + { + ShipType.Resupplier => "resupplier", + ShipType.Miner => "miner", + ShipType.Carrier => "carrier", + ShipType.Fighter => "fighter", + ShipType.HeavyFighter => "heavyfighter", + ShipType.Destroyer => "destroyer", + ShipType.LargeMiner => "largeminer", + ShipType.Freighter => "freighter", + ShipType.Bomber => "bomber", + ShipType.Scavenger => "scavenger", + ShipType.Frigate => "frigate", + ShipType.Transporter => "transporter", + ShipType.Interceptor => "interceptor", + ShipType.Scout => "scout", + ShipType.Courier => "courier", + ShipType.Builder => "builder", + ShipType.Corvette => "corvette", + ShipType.Police => "police", + ShipType.Battleship => "battleship", + ShipType.Gunboat => "gunboat", + ShipType.Tug => "tug", + ShipType.Compactor => "compactor", + _ => type.ToString(), + }; +} diff --git a/apps/backend/Shared/Runtime/ShipAutomationCatalog.cs b/apps/backend/Shared/Runtime/ShipAutomationCatalog.cs new file mode 100644 index 0000000..6d8986e --- /dev/null +++ b/apps/backend/Shared/Runtime/ShipAutomationCatalog.cs @@ -0,0 +1,121 @@ +namespace SpaceGame.Api.Shared.Runtime; + +public enum ShipAutomationSupportStatus +{ + Supported, + PartiallySupported, + NotSupported, + InternalOnly, +} + +public sealed record ShipBehaviorDefinition( + string Id, + string Label, + string Category, + ShipAutomationSupportStatus SupportStatus, + string Notes); + +public sealed record ShipOrderDefinition( + string Id, + string Label, + string Category, + ShipAutomationSupportStatus SupportStatus, + string Notes); + +public static class ShipBehaviorKinds +{ + public const string Patrol = "patrol"; + public const string Police = "police"; + public const string ProtectPosition = "protect-position"; + public const string ProtectShip = "protect-ship"; + public const string ProtectStation = "protect-station"; + + public const string LocalAutoMine = "local-auto-mine"; + public const string AdvancedAutoMine = "advanced-auto-mine"; + public const string ExpertAutoMine = "expert-auto-mine"; + + public const string DockAndWait = "dock-and-wait"; + public const string FlyAndWait = "fly-and-wait"; + public const string FlyToObject = "fly-to-object"; + public const string FollowShip = "follow-ship"; + public const string HoldPosition = "hold-position"; + + public const string AutoSalvage = "auto-salvage"; + + public const string LocalAutoTrade = "local-auto-trade"; + public const string AdvancedAutoTrade = "advanced-auto-trade"; + public const string FillShortages = "fill-shortages"; + public const string FindBuildTasks = "find-build-tasks"; + public const string RevisitKnownStations = "revisit-known-stations"; + public const string SupplyFleet = "supply-fleet"; + + public const string RepeatOrders = "repeat-orders"; + + public const string AttackTarget = "attack-target"; + public const string ConstructStation = "construct-station"; + public const string Idle = "idle"; +} + +public static class ShipAutomationCatalog +{ + public static readonly IReadOnlyList Behaviors = + [ + new(ShipBehaviorKinds.Patrol, "Patrol", "Combat", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing attack or fly-and-wait orders from the active patrol context."), + new(ShipBehaviorKinds.Police, "Police", "Combat", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing attack or follow-ship inspection orders from the active policing context."), + new(ShipBehaviorKinds.ProtectPosition, "Protect Position", "Combat", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing attack or fly-and-wait orders from the defended position context."), + new(ShipBehaviorKinds.ProtectShip, "Protect Ship", "Combat", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing attack or follow-ship escort orders from the guarded ship context."), + new(ShipBehaviorKinds.ProtectStation, "Protect Station", "Combat", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing attack or fly-and-wait guard orders from the defended station context."), + + new(ShipBehaviorKinds.LocalAutoMine, "Local AutoMine", "Mining", ShipAutomationSupportStatus.PartiallySupported, "Queue-backed for solo mining; broader order-generation model still in progress."), + new(ShipBehaviorKinds.AdvancedAutoMine, "Advanced AutoMine", "Mining", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing an internal mine-and-deliver run order from the current mining opportunity."), + new(ShipBehaviorKinds.ExpertAutoMine, "Expert AutoMine", "Mining", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing an internal mine-and-deliver run order from the current mining opportunity."), + + new(ShipBehaviorKinds.DockAndWait, "Dock And Wait", "Navigation", ShipAutomationSupportStatus.Supported, "Queue-backed behavior using the same building-block order as the direct order."), + new(ShipBehaviorKinds.FlyAndWait, "Fly And Wait", "Navigation", ShipAutomationSupportStatus.Supported, "Queue-backed behavior using the same building-block order as the direct order."), + new(ShipBehaviorKinds.FlyToObject, "Fly To Object", "Navigation", ShipAutomationSupportStatus.Supported, "Queue-backed behavior using the same building-block order as the direct order."), + new(ShipBehaviorKinds.FollowShip, "Follow Ship", "Navigation", ShipAutomationSupportStatus.Supported, "Queue-backed behavior using the same building-block order as the direct order."), + new(ShipBehaviorKinds.HoldPosition, "Hold Position", "Navigation", ShipAutomationSupportStatus.Supported, "Default baseline behavior; queue-backed behavior order is active."), + + new(ShipBehaviorKinds.AutoSalvage, "AutoSalvage", "Salvage", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing an internal salvage run order for wreck recovery."), + + new(ShipBehaviorKinds.LocalAutoTrade, "Local AutoTrade", "Trade", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing trade-route or dock-and-wait orders from the current market context."), + new(ShipBehaviorKinds.AdvancedAutoTrade, "Advanced AutoTrade", "Trade", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing trade-route orders from the current market context."), + new(ShipBehaviorKinds.FillShortages, "Fill Shortages", "Trade", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing trade-route orders from the current market context."), + new(ShipBehaviorKinds.FindBuildTasks, "Find Build Tasks", "Trade", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing construction-support trade routes from the current market context."), + new(ShipBehaviorKinds.RevisitKnownStations, "Revisit Known Stations", "Trade", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing trade-route or dock-and-wait orders from known-station context."), + new(ShipBehaviorKinds.SupplyFleet, "Supply Fleet", "Trade", ShipAutomationSupportStatus.Supported, "Queue-backed behavior synthesizing an internal fleet supply run order."), + + new(ShipBehaviorKinds.RepeatOrders, "Repeat Orders", "Advanced", ShipAutomationSupportStatus.Supported, "Queue-backed behavior generating the current repeat-order template at the bottom of the stack."), + + new(ShipBehaviorKinds.AttackTarget, "Attack Target", "Internal", ShipAutomationSupportStatus.InternalOnly, "Internal gameplay behavior used by current combat/control systems, not an X4 exposed default behavior."), + new(ShipBehaviorKinds.ConstructStation, "Construct Station", "Internal", ShipAutomationSupportStatus.InternalOnly, "Internal gameplay behavior used by construction ships."), + new(ShipBehaviorKinds.Idle, "Idle", "Internal", ShipAutomationSupportStatus.InternalOnly, "Legacy fallback/internal placeholder; not intended as an exposed player behavior."), + ]; + + public static readonly IReadOnlyList Orders = + [ + new(ShipOrderKinds.DockAndWait, "Dock And Wait", "Navigation", ShipAutomationSupportStatus.PartiallySupported, "Direct order supported in backend."), + new(ShipOrderKinds.FlyAndWait, "Fly To And Wait", "Navigation", ShipAutomationSupportStatus.PartiallySupported, "Direct order supported in backend."), + new(ShipOrderKinds.FlyToObject, "Fly To Object", "Navigation", ShipAutomationSupportStatus.PartiallySupported, "Direct order supported in backend."), + new(ShipOrderKinds.FollowShip, "Follow Ship", "Navigation", ShipAutomationSupportStatus.PartiallySupported, "Direct order supported in backend."), + new(ShipOrderKinds.HoldPosition, "Hold Position", "Navigation", ShipAutomationSupportStatus.Supported, "Direct order supported in backend."), + new(ShipOrderKinds.Move, "Move", "Navigation", ShipAutomationSupportStatus.PartiallySupported, "Low-level direct movement order; viewer may present richer labels such as Fly To And Wait instead."), + + new(ShipOrderKinds.AttackTarget, "Attack Target", "Combat", ShipAutomationSupportStatus.PartiallySupported, "Direct order supported in backend."), + + new(ShipOrderKinds.MineAndDeliver, "Mine Resource", "Mining", ShipAutomationSupportStatus.Supported, "Direct order mines the requested ware in the requested system until cargo is full."), + + new(ShipOrderKinds.TradeRoute, "Trade Route", "Trade", ShipAutomationSupportStatus.PartiallySupported, "Direct order supported in backend."), + + new(ShipOrderKinds.BuildAtSite, "Build At Site", "Construction", ShipAutomationSupportStatus.PartiallySupported, "Direct order supported in backend."), + + new(ShipOrderKinds.RepeatOrders, "Repeat Orders", "Advanced", ShipAutomationSupportStatus.PartiallySupported, "Represented today as a behavior plus templates, not a normal one-shot direct order."), + + new(ShipOrderKinds.MineLocal, "Mine Local", "Internal", ShipAutomationSupportStatus.InternalOnly, "Internal queue-backed behavior order for Local AutoMine."), + new(ShipOrderKinds.MineAndDeliverRun, "Mine And Deliver Run", "Internal", ShipAutomationSupportStatus.InternalOnly, "Internal queue-backed behavior order for Advanced/Expert AutoMine."), + new(ShipOrderKinds.SellMinedCargo, "Sell Mined Cargo", "Internal", ShipAutomationSupportStatus.InternalOnly, "Internal queue-backed behavior order for Local AutoMine."), + new(ShipOrderKinds.SupplyFleetRun, "Supply Fleet Run", "Internal", ShipAutomationSupportStatus.InternalOnly, "Internal queue-backed behavior order for Supply Fleet."), + new(ShipOrderKinds.SalvageRun, "Salvage Run", "Internal", ShipAutomationSupportStatus.InternalOnly, "Internal queue-backed behavior order for AutoSalvage."), + new(ShipOrderKinds.Flee, "Flee", "Internal", ShipAutomationSupportStatus.InternalOnly, "Internal emergency order."), + ]; +} diff --git a/apps/backend/Shared/Runtime/SimulationKinds.cs b/apps/backend/Shared/Runtime/SimulationKinds.cs index 6542e21..95575d0 100644 --- a/apps/backend/Shared/Runtime/SimulationKinds.cs +++ b/apps/backend/Shared/Runtime/SimulationKinds.cs @@ -28,6 +28,13 @@ public enum OrderStatus Interrupted, } +public enum ShipOrderSourceKind +{ + Player, + Behavior, + Commander, +} + public enum AiPlanStatus { Planned, @@ -166,6 +173,11 @@ public static class ShipOrderKinds public const string BuildAtSite = "build-at-site"; public const string AttackTarget = "attack-target"; public const string HoldPosition = "hold-position"; + public const string MineLocal = "mine-local"; + public const string MineAndDeliverRun = "mine-and-deliver-run"; + public const string SellMinedCargo = "sell-mined-cargo"; + public const string SupplyFleetRun = "supply-fleet-run"; + public const string SalvageRun = "salvage-run"; public const string RepeatOrders = "repeat-orders"; public const string Flee = "flee"; } @@ -329,6 +341,14 @@ public static class SimulationEnumMappings _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), }; + public static string ToContractValue(this ShipOrderSourceKind kind) => kind switch + { + ShipOrderSourceKind.Player => "player", + ShipOrderSourceKind.Behavior => "behavior", + ShipOrderSourceKind.Commander => "commander", + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), + }; + public static string ToContractValue(this ShipState state) => state switch { ShipState.Idle => "idle", diff --git a/apps/backend/Shared/Runtime/SimulationRuntimeSupport.cs b/apps/backend/Shared/Runtime/SimulationRuntimeSupport.cs index 549a698..fb3afa8 100644 --- a/apps/backend/Shared/Runtime/SimulationRuntimeSupport.cs +++ b/apps/backend/Shared/Runtime/SimulationRuntimeSupport.cs @@ -3,8 +3,56 @@ namespace SpaceGame.Api.Shared.Runtime; internal static class SimulationRuntimeSupport { - internal static bool HasShipCapabilities(ShipDefinition definition, params string[] capabilities) => - capabilities.All(cap => definition.Capabilities.Contains(cap, StringComparer.Ordinal)); + internal static bool CanWarp(ShipDefinition definition) => + definition.Engines.Count > 0; + + internal static bool CanFtl(ShipDefinition definition) => + definition.Engines.Count > 0; + + internal static bool IsMiningShip(ShipDefinition definition) => + definition.Type is ShipType.Miner or ShipType.LargeMiner; + + internal static bool IsTransportShip(ShipDefinition definition) => + definition.Type is ShipType.Freighter or ShipType.Transporter or ShipType.Courier or ShipType.Resupplier; + + internal static bool IsConstructionShip(ShipDefinition definition) => + definition.Type == ShipType.Builder; + + internal static bool IsMilitaryShip(ShipDefinition definition) => + definition.Type is ShipType.Fighter + or ShipType.HeavyFighter + or ShipType.Destroyer + or ShipType.Bomber + or ShipType.Frigate + or ShipType.Interceptor + or ShipType.Corvette + or ShipType.Battleship + or ShipType.Gunboat; + + internal static string? GetShipCategory(ShipDefinition definition) + { + if (IsMilitaryShip(definition)) + { + return "military"; + } + + if (IsConstructionShip(definition)) + { + return "construction"; + } + + if (IsTransportShip(definition)) + { + return "transport"; + } + + if (IsMiningShip(definition)) + { + return "mining"; + } + + return null; + } internal static int CountStationModules(StationRuntime station, ModuleType moduleType) => station.Modules.Count(module => module.ModuleType == moduleType); @@ -131,13 +179,13 @@ internal static class SimulationRuntimeSupport modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal))); internal static bool CanExtractNode(ShipRuntime ship, ResourceNodeRuntime node, SimulationWorld world) => - HasShipCapabilities(ship.Definition, "mining") + IsMiningShip(ship.Definition) && world.ItemDefinitions.TryGetValue(node.ItemId, out var item) && item.CargoKind is not null - && item.CargoKind == ship.Definition.CargoKind; + && ship.Definition.SupportsCargoKind(item.CargoKind.Value); internal static bool CanBuildClaimBeacon(ShipRuntime ship) => - string.Equals(ship.Definition.Kind, "military", StringComparison.Ordinal); + IsMilitaryShip(ship.Definition); internal static float ComputeWorkforceRatio(float population, float workforceRequired) { diff --git a/apps/backend/Ships/AI/ShipAiService.BehaviorQueue.cs b/apps/backend/Ships/AI/ShipAiService.BehaviorQueue.cs new file mode 100644 index 0000000..f36e36d --- /dev/null +++ b/apps/backend/Ships/AI/ShipAiService.BehaviorQueue.cs @@ -0,0 +1,784 @@ +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; +using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; + +namespace SpaceGame.Api.Ships.AI; + +public sealed partial class ShipAiService +{ + private static ShipOrderRuntime? GetTopOrder(ShipRuntime ship) => + ship.OrderQueue + .Where(order => order.Status is OrderStatus.Queued or OrderStatus.Active) + .OrderByDescending(GetOrderSourcePriority) + .ThenByDescending(order => order.Priority) + .ThenBy(order => order.CreatedAtUtc) + .FirstOrDefault(); + + private static int GetOrderSourcePriority(ShipOrderRuntime order) => order.SourceKind switch + { + ShipOrderSourceKind.Player => 300, + ShipOrderSourceKind.Commander => 200, + ShipOrderSourceKind.Behavior => 100, + _ => 0, + }; + + private void SyncBehaviorOrders(SimulationWorld world, ShipRuntime ship) + { + var desiredOrder = BuildManagedBehaviorOrder(world, ship); + ship.OrderQueue.RemoveAll(order => + order.SourceKind == ShipOrderSourceKind.Behavior + && (desiredOrder is null || !string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal))); + + if (desiredOrder is null) + { + return; + } + + var existing = ship.OrderQueue.FirstOrDefault(order => string.Equals(order.Id, desiredOrder.Id, StringComparison.Ordinal)); + if (existing is null) + { + ship.OrderQueue.Add(desiredOrder); + return; + } + + if (ManagedOrdersEqual(existing, desiredOrder)) + { + return; + } + + ship.OrderQueue.Remove(existing); + ship.OrderQueue.Add(desiredOrder); + } + + private ShipOrderRuntime? BuildManagedBehaviorOrder(SimulationWorld world, ShipRuntime ship) + { + var assignment = ResolveAssignment(world, ship); + var behaviorKind = assignment?.BehaviorKind ?? ship.DefaultBehavior.Kind; + var systemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; + + if (string.Equals(behaviorKind, HoldPosition, StringComparison.Ordinal)) + { + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-hold-position", + Kind = ShipOrderKinds.HoldPosition, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = "Hold position", + TargetSystemId = systemId, + TargetPosition = ship.DefaultBehavior.TargetPosition ?? ship.Position, + WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), + Radius = ship.DefaultBehavior.Radius, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, DockAndWait, StringComparison.Ordinal)) + { + var station = ResolveStation(world, assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId ?? ship.DefaultBehavior.HomeStationId); + if (station is null) + { + ship.LastAccessFailureReason = "station-missing"; + return null; + } + + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-dock-and-wait", + Kind = ShipOrderKinds.DockAndWait, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = $"Dock and wait at {station.Label}", + TargetEntityId = station.Id, + TargetSystemId = station.SystemId, + DestinationStationId = station.Id, + WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), + Radius = ship.DefaultBehavior.Radius, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, FlyAndWait, StringComparison.Ordinal)) + { + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-fly-and-wait", + Kind = ShipOrderKinds.FlyAndWait, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = "Fly and wait", + TargetSystemId = systemId, + TargetPosition = ship.DefaultBehavior.TargetPosition ?? ship.Position, + WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), + Radius = ship.DefaultBehavior.Radius, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, FollowShip, StringComparison.Ordinal)) + { + var targetShip = world.Ships.FirstOrDefault(candidate => + candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId) + && candidate.Health > 0f); + if (targetShip is null) + { + ship.LastAccessFailureReason = "target-ship-missing"; + return null; + } + + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-follow-ship", + Kind = ShipOrderKinds.FollowShip, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = $"Follow {targetShip.Definition.Name}", + TargetEntityId = targetShip.Id, + TargetSystemId = targetShip.SystemId, + WaitSeconds = MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), + Radius = MathF.Max(16f, ship.DefaultBehavior.Radius), + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, FlyToObject, StringComparison.Ordinal)) + { + var targetEntityId = assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId; + var target = ResolveObjectTarget(world, targetEntityId); + if (target is null) + { + ship.LastAccessFailureReason = "target-missing"; + return null; + } + + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-fly-to-object", + Kind = ShipOrderKinds.FlyToObject, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = "Fly to object", + TargetEntityId = targetEntityId, + TargetSystemId = target.Value.SystemId, + TargetPosition = target.Value.Position, + WaitSeconds = MathF.Max(1f, ship.DefaultBehavior.WaitSeconds), + Radius = MathF.Max(8f, ship.DefaultBehavior.Radius), + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, Patrol, StringComparison.Ordinal)) + { + return BuildManagedPatrolOrder(world, ship, assignment, behaviorKind); + } + + if (string.Equals(behaviorKind, AttackTarget, StringComparison.Ordinal)) + { + var targetEntityId = assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId; + if (string.IsNullOrWhiteSpace(targetEntityId)) + { + return BuildManagedPatrolOrder(world, ship, assignment, behaviorKind); + } + + var target = ResolveObjectTarget(world, targetEntityId); + ship.LastAccessFailureReason = target is null ? "target-missing" : null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-attack-target", + Kind = ShipOrderKinds.AttackTarget, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = "Attack target", + TargetEntityId = targetEntityId, + TargetSystemId = target?.SystemId ?? assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId, + TargetPosition = target?.Position ?? ship.Position, + WaitSeconds = 0f, + Radius = 26f, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, ConstructStation, StringComparison.Ordinal)) + { + var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.PreferredConstructionSiteId)) + ?? world.ConstructionSites + .Where(candidate => candidate.FactionId == ship.FactionId && candidate.State is ConstructionSiteStateKinds.Active or ConstructionSiteStateKinds.Planned) + .OrderBy(candidate => candidate.Id, StringComparer.Ordinal) + .FirstOrDefault(); + if (site is null) + { + ship.LastAccessFailureReason = "no-construction-site"; + return null; + } + + if (ResolveSupportStation(world, ship, site) is null) + { + ship.LastAccessFailureReason = "support-station-missing"; + return null; + } + + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-construct-station", + Kind = ShipOrderKinds.BuildAtSite, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = $"Build {site.BlueprintId}", + TargetEntityId = site.Id, + TargetSystemId = site.SystemId, + ConstructionSiteId = site.Id, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, AdvancedAutoMine, StringComparison.Ordinal) + || string.Equals(behaviorKind, ExpertAutoMine, StringComparison.Ordinal)) + { + var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); + if (homeStation is null) + { + ship.LastAccessFailureReason = "no-home-station"; + return null; + } + + var opportunity = SelectMiningOpportunity(world, ship, homeStation, assignment, behaviorKind); + if (opportunity is null) + { + ship.LastAccessFailureReason = "no-mineable-node"; + return null; + } + + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-{behaviorKind}-mine-and-deliver", + Kind = ShipOrderKinds.MineAndDeliverRun, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = opportunity.Summary, + TargetEntityId = opportunity.Node.Id, + TargetSystemId = opportunity.Node.SystemId, + DestinationStationId = opportunity.DropOffStation.Id, + ItemId = opportunity.Node.ItemId, + NodeId = opportunity.Node.Id, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, ProtectPosition, StringComparison.Ordinal)) + { + var targetSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; + var targetPosition = ship.DefaultBehavior.TargetPosition ?? assignment?.TargetPosition ?? ship.Position; + var threat = SelectThreatTarget(world, ship, targetSystemId, targetPosition, MathF.Max(90f, ship.DefaultBehavior.Radius)); + if (threat is not null) + { + ship.LastAccessFailureReason = null; + return CreateManagedAttackOrder(ship, behaviorKind, "Protect position", threat.EntityId, threat.SystemId, threat.Position); + } + + ship.LastAccessFailureReason = null; + return CreateManagedFlyAndWaitOrder( + ship, + behaviorKind, + "Protect position", + targetSystemId, + targetPosition, + MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), + MathF.Max(6f, ship.DefaultBehavior.Radius)); + } + + if (string.Equals(behaviorKind, ProtectShip, StringComparison.Ordinal)) + { + var guardTarget = world.Ships.FirstOrDefault(candidate => + candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId) + && candidate.Health > 0f); + if (guardTarget is null) + { + return BuildManagedPatrolOrder(world, ship, assignment, Patrol); + } + + var threat = SelectThreatTarget( + world, + ship, + guardTarget.SystemId, + guardTarget.Position, + MathF.Max(90f, ship.DefaultBehavior.Radius), + excludeEntityId: guardTarget.Id); + if (threat is not null) + { + ship.LastAccessFailureReason = null; + return CreateManagedAttackOrder(ship, behaviorKind, $"Protect {guardTarget.Definition.Name}", threat.EntityId, threat.SystemId, threat.Position); + } + + ship.LastAccessFailureReason = null; + return CreateManagedFollowShipOrder( + ship, + behaviorKind, + $"Escort {guardTarget.Definition.Name}", + guardTarget, + MathF.Max(18f, ship.DefaultBehavior.Radius * 0.5f), + MathF.Max(2f, ship.DefaultBehavior.WaitSeconds)); + } + + if (string.Equals(behaviorKind, ProtectStation, StringComparison.Ordinal)) + { + var station = ResolveStation(world, assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId ?? assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); + if (station is null) + { + return BuildManagedPatrolOrder(world, ship, assignment, Patrol); + } + + var threat = SelectThreatTarget(world, ship, station.SystemId, station.Position, MathF.Max(station.Radius + 80f, ship.DefaultBehavior.Radius)); + if (threat is not null) + { + ship.LastAccessFailureReason = null; + return CreateManagedAttackOrder(ship, behaviorKind, $"Protect {station.Label}", threat.EntityId, threat.SystemId, threat.Position); + } + + ship.LastAccessFailureReason = null; + return CreateManagedFlyAndWaitOrder( + ship, + behaviorKind, + $"Guard {station.Label}", + station.SystemId, + GetFormationPosition(station.Position, ship.Id, MathF.Max(station.Radius + 18f, ship.DefaultBehavior.Radius)), + MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), + MathF.Max(6f, ship.DefaultBehavior.Radius)); + } + + if (string.Equals(behaviorKind, Police, StringComparison.Ordinal)) + { + var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); + var policeSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? homeStation?.SystemId ?? ship.SystemId; + var areaPosition = homeStation?.Position ?? ship.DefaultBehavior.TargetPosition ?? ship.Position; + var contact = SelectPoliceContact(world, ship, policeSystemId, areaPosition, MathF.Max(80f, ship.DefaultBehavior.Radius)); + if (contact is null) + { + return BuildManagedPatrolOrder(world, ship, assignment, Patrol); + } + + ship.LastAccessFailureReason = null; + return contact.Engage + ? CreateManagedAttackOrder(ship, behaviorKind, "Police engage", contact.EntityId, contact.SystemId, contact.Position) + : CreateManagedFollowTargetOrder(ship, behaviorKind, "Police inspect", contact.EntityId, contact.SystemId, contact.Position, MathF.Max(14f, ship.DefaultBehavior.Radius * 0.5f), MathF.Max(2f, ship.DefaultBehavior.WaitSeconds)); + } + + if (string.Equals(behaviorKind, LocalAutoTrade, StringComparison.Ordinal) + || string.Equals(behaviorKind, AdvancedAutoTrade, StringComparison.Ordinal) + || string.Equals(behaviorKind, FillShortages, StringComparison.Ordinal) + || string.Equals(behaviorKind, FindBuildTasks, StringComparison.Ordinal) + || string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal)) + { + var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); + var route = SelectTradeRoute(world, ship, homeStation, behaviorKind, ship.DefaultBehavior.KnownStationsOnly); + if (route is not null) + { + ship.LastAccessFailureReason = null; + return CreateManagedTradeRouteOrder(ship, behaviorKind, route); + } + + if (string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal) + && SelectKnownStationVisit(world, ship, homeStation) is { } visitStation) + { + ship.LastAccessFailureReason = null; + return CreateManagedDockAndWaitOrder(ship, behaviorKind, visitStation, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), $"Revisit {visitStation.Label}"); + } + + ship.LastAccessFailureReason = "no-trade-route"; + return null; + } + + if (string.Equals(behaviorKind, SupplyFleet, StringComparison.Ordinal)) + { + var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); + var plan = SelectFleetSupplyPlan(world, ship, homeStation); + if (plan is null) + { + ship.LastAccessFailureReason = "no-fleet-to-supply"; + return null; + } + + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-supply-fleet", + Kind = ShipOrderKinds.SupplyFleetRun, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = plan.Summary, + TargetEntityId = plan.TargetShip.Id, + TargetSystemId = plan.TargetShip.SystemId, + SourceStationId = plan.SourceStation.Id, + ItemId = plan.ItemId, + Radius = plan.Radius, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, AutoSalvage, StringComparison.Ordinal)) + { + var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); + if (homeStation is null) + { + ship.LastAccessFailureReason = "no-home-station"; + return null; + } + + var salvage = SelectSalvageOpportunity(world, ship, homeStation); + if (salvage is null) + { + ship.LastAccessFailureReason = "no-salvage-target"; + return null; + } + + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-auto-salvage", + Kind = ShipOrderKinds.SalvageRun, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = salvage.Summary, + TargetEntityId = salvage.Wreck.Id, + TargetSystemId = salvage.Wreck.SystemId, + TargetPosition = salvage.Wreck.Position, + SourceStationId = homeStation.Id, + ItemId = salvage.Wreck.ItemId, + Radius = MathF.Max(8f, ship.DefaultBehavior.Radius * 0.25f), + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + if (string.Equals(behaviorKind, RepeatOrders, StringComparison.Ordinal)) + { + if (ship.DefaultBehavior.RepeatOrders.Count == 0) + { + ship.LastAccessFailureReason = "no-repeat-orders"; + return null; + } + + var template = ship.DefaultBehavior.RepeatOrders[ship.DefaultBehavior.RepeatIndex % ship.DefaultBehavior.RepeatOrders.Count]; + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-repeat-{ship.DefaultBehavior.RepeatIndex}", + Kind = template.Kind, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = template.Label, + TargetEntityId = template.TargetEntityId, + TargetSystemId = template.TargetSystemId, + TargetPosition = template.TargetPosition, + SourceStationId = template.SourceStationId, + DestinationStationId = template.DestinationStationId, + ItemId = template.ItemId, + NodeId = template.NodeId, + ConstructionSiteId = template.ConstructionSiteId, + ModuleId = template.ModuleId, + WaitSeconds = template.WaitSeconds, + Radius = template.Radius, + MaxSystemRange = template.MaxSystemRange ?? ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = template.KnownStationsOnly, + }; + } + + if (!string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal)) + { + return null; + } + + var itemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId; + if (string.IsNullOrWhiteSpace(itemId)) + { + ship.LastAccessFailureReason = "missing-item"; + return null; + } + + if (GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f) + { + var buyer = SelectLocalAutoMineBuyer(world, ship, systemId, itemId); + if (buyer is null) + { + ship.LastAccessFailureReason = "no-suitable-buyer"; + return null; + } + + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-local-auto-mine-sell", + Kind = ShipOrderKinds.SellMinedCargo, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = $"Sell {itemId} in {systemId}", + TargetEntityId = buyer.Id, + TargetSystemId = buyer.SystemId, + DestinationStationId = buyer.Id, + ItemId = itemId, + WaitSeconds = 0f, + Radius = 0f, + MaxSystemRange = 0, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + var node = SelectLocalMiningNode(world, ship, systemId, itemId); + if (node is null) + { + ship.LastAccessFailureReason = "no-mineable-node"; + return null; + } + + ship.LastAccessFailureReason = null; + return new ShipOrderRuntime + { + Id = $"behavior-{ship.Id}-local-auto-mine-mine", + Kind = ShipOrderKinds.MineLocal, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = $"Mine {itemId} in {systemId}", + TargetSystemId = node.SystemId, + NodeId = node.Id, + ItemId = node.ItemId, + WaitSeconds = 0f, + Radius = 0f, + MaxSystemRange = 0, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + } + + private static bool ManagedOrdersEqual(ShipOrderRuntime left, ShipOrderRuntime right) => + string.Equals(left.Id, right.Id, StringComparison.Ordinal) + && string.Equals(left.Kind, right.Kind, StringComparison.Ordinal) + && left.SourceKind == right.SourceKind + && string.Equals(left.SourceId, right.SourceId, StringComparison.Ordinal) + && left.Priority == right.Priority + && left.InterruptCurrentPlan == right.InterruptCurrentPlan + && string.Equals(left.Label, right.Label, StringComparison.Ordinal) + && string.Equals(left.TargetEntityId, right.TargetEntityId, StringComparison.Ordinal) + && string.Equals(left.TargetSystemId, right.TargetSystemId, StringComparison.Ordinal) + && left.TargetPosition == right.TargetPosition + && string.Equals(left.DestinationStationId, right.DestinationStationId, StringComparison.Ordinal) + && string.Equals(left.ItemId, right.ItemId, StringComparison.Ordinal) + && string.Equals(left.NodeId, right.NodeId, StringComparison.Ordinal) + && left.WaitSeconds.Equals(right.WaitSeconds) + && left.Radius.Equals(right.Radius) + && left.MaxSystemRange == right.MaxSystemRange + && left.KnownStationsOnly == right.KnownStationsOnly; + + private ShipOrderRuntime BuildManagedPatrolOrder(SimulationWorld world, ShipRuntime ship, CommanderAssignmentRuntime? assignment, string sourceKind) + { + var patrolSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; + var protectPosition = ship.DefaultBehavior.TargetPosition ?? assignment?.TargetPosition ?? ship.Position; + var patrolThreat = SelectThreatTarget(world, ship, patrolSystemId, protectPosition, MathF.Max(60f, ship.DefaultBehavior.Radius)); + if (patrolThreat is not null) + { + ship.LastAccessFailureReason = null; + return CreateManagedAttackOrder(ship, sourceKind, "Patrol intercept", patrolThreat.EntityId, patrolThreat.SystemId, patrolThreat.Position, orderIdSuffix: "patrol-attack"); + } + + Vector3 targetPosition; + string targetSystemId; + if (ship.DefaultBehavior.PatrolPoints.Count > 0) + { + var index = ship.DefaultBehavior.PatrolIndex % ship.DefaultBehavior.PatrolPoints.Count; + targetPosition = ship.DefaultBehavior.PatrolPoints[index]; + ship.DefaultBehavior.PatrolIndex = (index + 1) % ship.DefaultBehavior.PatrolPoints.Count; + targetSystemId = ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; + } + else if (ResolveStation(world, ship.DefaultBehavior.HomeStationId ?? assignment?.HomeStationId) is { } homeStation) + { + var patrolRadius = homeStation.Radius + 90f; + targetPosition = new Vector3(homeStation.Position.X + patrolRadius, homeStation.Position.Y, homeStation.Position.Z); + targetSystemId = homeStation.SystemId; + } + else + { + targetPosition = ship.Position; + targetSystemId = ship.SystemId; + } + + ship.LastAccessFailureReason = null; + return CreateManagedFlyAndWaitOrder(ship, sourceKind, "Patrol waypoint", targetSystemId, targetPosition, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), MathF.Max(6f, ship.DefaultBehavior.Radius), orderIdSuffix: "patrol-fly-and-wait"); + } + + private static ShipOrderRuntime CreateManagedAttackOrder( + ShipRuntime ship, + string behaviorKind, + string label, + string targetEntityId, + string targetSystemId, + Vector3 targetPosition, + string? orderIdSuffix = null) => + new() + { + Id = $"behavior-{ship.Id}-{orderIdSuffix ?? behaviorKind}", + Kind = ShipOrderKinds.AttackTarget, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = label, + TargetEntityId = targetEntityId, + TargetSystemId = targetSystemId, + TargetPosition = targetPosition, + WaitSeconds = 0f, + Radius = 26f, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + + private static ShipOrderRuntime CreateManagedTradeRouteOrder(ShipRuntime ship, string behaviorKind, TradeRoutePlan route) => + new() + { + Id = $"behavior-{ship.Id}-{behaviorKind}-trade-route", + Kind = ShipOrderKinds.TradeRoute, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = route.Summary, + SourceStationId = route.SourceStation.Id, + DestinationStationId = route.DestinationStation.Id, + ItemId = route.ItemId, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + + private static ShipOrderRuntime CreateManagedDockAndWaitOrder(ShipRuntime ship, string behaviorKind, StationRuntime station, float waitSeconds, string label) => + new() + { + Id = $"behavior-{ship.Id}-{behaviorKind}-dock-and-wait", + Kind = ShipOrderKinds.DockAndWait, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = label, + TargetEntityId = station.Id, + TargetSystemId = station.SystemId, + DestinationStationId = station.Id, + WaitSeconds = waitSeconds, + Radius = ship.DefaultBehavior.Radius, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + + private static ShipOrderRuntime CreateManagedFlyAndWaitOrder( + ShipRuntime ship, + string behaviorKind, + string label, + string targetSystemId, + Vector3 targetPosition, + float waitSeconds, + float radius, + string? orderIdSuffix = null) => + new() + { + Id = $"behavior-{ship.Id}-{orderIdSuffix ?? behaviorKind}", + Kind = ShipOrderKinds.FlyAndWait, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = label, + TargetSystemId = targetSystemId, + TargetPosition = targetPosition, + WaitSeconds = waitSeconds, + Radius = radius, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + + private static ShipOrderRuntime CreateManagedFollowShipOrder( + ShipRuntime ship, + string behaviorKind, + string label, + ShipRuntime targetShip, + float radius, + float waitSeconds) => + new() + { + Id = $"behavior-{ship.Id}-{behaviorKind}", + Kind = ShipOrderKinds.FollowShip, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = label, + TargetEntityId = targetShip.Id, + TargetSystemId = targetShip.SystemId, + WaitSeconds = waitSeconds, + Radius = radius, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; + + private static ShipOrderRuntime CreateManagedFollowTargetOrder( + ShipRuntime ship, + string behaviorKind, + string label, + string targetEntityId, + string targetSystemId, + Vector3 targetPosition, + float radius, + float waitSeconds) => + new() + { + Id = $"behavior-{ship.Id}-{behaviorKind}", + Kind = ShipOrderKinds.FollowShip, + SourceKind = ShipOrderSourceKind.Behavior, + SourceId = behaviorKind, + Priority = 0, + InterruptCurrentPlan = false, + Label = label, + TargetEntityId = targetEntityId, + TargetSystemId = targetSystemId, + TargetPosition = targetPosition, + WaitSeconds = waitSeconds, + Radius = radius, + MaxSystemRange = ship.DefaultBehavior.MaxSystemRange, + KnownStationsOnly = ship.DefaultBehavior.KnownStationsOnly, + }; +} diff --git a/apps/backend/Ships/AI/ShipAiService.Data.cs b/apps/backend/Ships/AI/ShipAiService.Data.cs new file mode 100644 index 0000000..0661240 --- /dev/null +++ b/apps/backend/Ships/AI/ShipAiService.Data.cs @@ -0,0 +1,54 @@ +namespace SpaceGame.Api.Ships.AI; + +public sealed partial class ShipAiService +{ + private enum SubTaskOutcome + { + Active, + Completed, + Failed, + } + + private sealed record TradeRoutePlan( + StationRuntime SourceStation, + StationRuntime DestinationStation, + string ItemId, + float Score, + string Summary); + + private sealed record MiningOpportunity( + ResourceNodeRuntime Node, + StationRuntime DropOffStation, + float Score, + string Summary); + + private sealed record FleetSupplyPlan( + StationRuntime SourceStation, + ShipRuntime TargetShip, + string ItemId, + float Amount, + float Radius, + string Summary); + + private sealed record LocalMiningBuyerCandidate( + StationRuntime Station, + float Score); + + private sealed record ThreatTargetCandidate( + string EntityId, + string SystemId, + Vector3 Position, + float Score); + + private sealed record PoliceContactCandidate( + string EntityId, + string SystemId, + Vector3 Position, + bool Engage, + float Score); + + private sealed record SalvageOpportunity( + WreckRuntime Wreck, + float Score, + string Summary); +} diff --git a/apps/backend/Ships/AI/ShipAiService.Execution.cs b/apps/backend/Ships/AI/ShipAiService.Execution.cs new file mode 100644 index 0000000..83053e0 --- /dev/null +++ b/apps/backend/Ships/AI/ShipAiService.Execution.cs @@ -0,0 +1,770 @@ +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; +using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; +using static SpaceGame.Api.Stations.Simulation.StationSimulationService; + +namespace SpaceGame.Api.Ships.AI; + +public sealed partial class ShipAiService +{ + private SubTaskOutcome UpdateSubTask(SimulationWorld world, ShipRuntime ship, ShipPlanStepRuntime step, ShipSubTaskRuntime subTask, float deltaSeconds) + { + return subTask.Kind switch + { + var kind when string.Equals(kind, ShipTaskKinds.Travel, StringComparison.Ordinal) => UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: true), + var kind when string.Equals(kind, ShipTaskKinds.FollowTarget, StringComparison.Ordinal) => UpdateFollowSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.Dock, StringComparison.Ordinal) => UpdateDockSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.Undock, StringComparison.Ordinal) => UpdateUndockSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.LoadCargo, StringComparison.Ordinal) => UpdateLoadCargoSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.UnloadCargo, StringComparison.Ordinal) => UpdateUnloadCargoSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.TransferCargoToShip, StringComparison.Ordinal) => UpdateTransferCargoToShipSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.MineNode, StringComparison.Ordinal) => UpdateMineSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.SalvageWreck, StringComparison.Ordinal) => UpdateSalvageSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.DeliverConstruction, StringComparison.Ordinal) => UpdateDeliverConstructionSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.BuildConstructionSite, StringComparison.Ordinal) => UpdateBuildConstructionSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.AttackTarget, StringComparison.Ordinal) => UpdateAttackSubTask(world, ship, subTask, deltaSeconds), + var kind when string.Equals(kind, ShipTaskKinds.HoldPosition, StringComparison.Ordinal) => UpdateHoldSubTask(ship, subTask, deltaSeconds), + _ => SubTaskOutcome.Failed, + }; + } + + private SubTaskOutcome UpdateHoldSubTask(ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + ship.State = ShipState.HoldingPosition; + ship.TargetPosition = subTask.TargetPosition ?? ship.Position; + ship.Position = ship.Position.MoveToward(ship.TargetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(ship.TargetPosition))); + return AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.1f, subTask.Amount)) ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateFollowSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f); + if (targetShip is null) + { + subTask.BlockingReason = "follow-target-missing"; + return SubTaskOutcome.Failed; + } + + var desiredPosition = GetFormationPosition(targetShip.Position, ship.Id, MathF.Max(subTask.Threshold, 16f)); + subTask.TargetSystemId = targetShip.SystemId; + subTask.TargetPosition = desiredPosition; + subTask.BlockingReason = null; + if (ship.SystemId != targetShip.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 12f)) + { + return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); + } + + ship.State = ShipState.HoldingPosition; + ship.TargetPosition = desiredPosition; + ship.Position = ship.Position.MoveToward(desiredPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(desiredPosition))); + return AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.5f, subTask.Amount)) ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateTravelSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds, bool completeOnArrival) + { + if (subTask.TargetPosition is null || subTask.TargetSystemId is null) + { + subTask.BlockingReason = "travel-target-missing"; + ship.State = ShipState.Blocked; + return SubTaskOutcome.Failed; + } + + var targetPosition = ResolveCurrentTargetPosition(world, subTask); + var targetCelestial = ResolveTravelTargetCelestial(world, subTask, targetPosition); + ship.TargetPosition = targetPosition; + + if (ship.SystemId != subTask.TargetSystemId) + { + if (!CanFtl(ship.Definition)) + { + subTask.BlockingReason = "ftl-unavailable"; + ship.State = ShipState.Blocked; + return SubTaskOutcome.Failed; + } + + var destinationEntryCelestial = ResolveSystemEntryCelestial(world, subTask.TargetSystemId); + var destinationEntryPosition = destinationEntryCelestial?.Position ?? targetPosition; + return UpdateFtlTransit(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, destinationEntryPosition, destinationEntryCelestial, completeOnArrival, targetPosition); + } + + var currentCelestial = ResolveCurrentCelestial(world, ship); + if (targetCelestial is not null + && currentCelestial is not null + && !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal)) + { + if (!CanWarp(ship.Definition)) + { + return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, completeOnArrival); + } + + return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival); + } + + if (targetCelestial is not null + && ship.Position.DistanceTo(targetPosition) > WarpEngageDistanceKilometers + && CanWarp(ship.Definition)) + { + return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival); + } + + return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, completeOnArrival); + } + + private SubTaskOutcome UpdateAttackSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + var hostileShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f); + var hostileStation = hostileShip is null + ? world.Stations.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) + : null; + if ((hostileShip is not null && hostileShip.FactionId == ship.FactionId) + || (hostileStation is not null && hostileStation.FactionId == ship.FactionId)) + { + subTask.BlockingReason = "friendly-target"; + return SubTaskOutcome.Failed; + } + + if (hostileShip is null && hostileStation is null) + { + return SubTaskOutcome.Completed; + } + + var targetSystemId = hostileShip?.SystemId ?? hostileStation!.SystemId; + var targetPosition = hostileShip?.Position ?? hostileStation!.Position; + var attackRange = hostileShip is null ? hostileStation!.Radius + 18f : 26f; + subTask.TargetSystemId = targetSystemId; + subTask.TargetPosition = targetPosition; + subTask.Threshold = attackRange; + + if (ship.SystemId != targetSystemId || ship.Position.DistanceTo(targetPosition) > attackRange) + { + return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); + } + + ship.State = ShipState.EngagingTarget; + ship.TargetPosition = targetPosition; + ship.Position = ship.Position.MoveToward(targetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, 8f)); + var damage = GetShipDamagePerSecond(ship) * deltaSeconds * GetSkillFactor(ship.Skills.Combat); + subTask.Progress = 1f; + + if (hostileShip is not null) + { + hostileShip.Health = MathF.Max(0f, hostileShip.Health - damage); + return hostileShip.Health <= 0f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } + + hostileStation!.Health = MathF.Max(0f, hostileStation.Health - (damage * 0.6f)); + return hostileStation.Health <= 0f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateMineSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + var node = ResolveNode(world, subTask.TargetEntityId ?? subTask.TargetNodeId); + if (node is null || !CanExtractNode(ship, node, world)) + { + subTask.BlockingReason = "node-missing"; + ship.State = ShipState.Blocked; + return SubTaskOutcome.Failed; + } + + var targetPosition = subTask.TargetPosition ?? GetResourceHoldPosition(node.Position, ship.Id, 20f); + ship.TargetPosition = targetPosition; + if (ship.Position.DistanceTo(targetPosition) > MathF.Max(subTask.Threshold, 8f)) + { + ship.State = ShipState.MiningApproach; + ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return SubTaskOutcome.Active; + } + + var cargoAmount = GetShipCargoAmount(ship); + if (cargoAmount >= ship.Definition.GetTotalCargoCapacity() - 0.01f) + { + return SubTaskOutcome.Completed; + } + + ship.State = ShipState.Mining; + if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.MiningCycleSeconds)) + { + return SubTaskOutcome.Active; + } + + var remainingCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - cargoAmount); + var mined = MathF.Min(balance.MiningRate * GetSkillFactor(ship.Skills.Mining), remainingCapacity); + mined = MathF.Min(mined, node.OreRemaining); + if (mined <= 0.01f) + { + return SubTaskOutcome.Completed; + } + + AddInventory(ship.Inventory, node.ItemId, mined); + node.OreRemaining = MathF.Max(0f, node.OreRemaining - mined); + if (GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f || node.OreRemaining <= 0.01f) + { + return SubTaskOutcome.Completed; + } + + subTask.ElapsedSeconds = 0f; + return SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateDockSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + var station = ResolveStation(world, subTask.TargetEntityId); + if (station is null) + { + subTask.BlockingReason = "dock-target-missing"; + ship.State = ShipState.Blocked; + return SubTaskOutcome.Failed; + } + + var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id); + if (padIndex is null) + { + ship.State = ShipState.AwaitingDock; + ship.TargetPosition = GetDockingHoldPosition(station, ship.Id); + if (ship.Position.DistanceTo(ship.TargetPosition) > 4f) + { + ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + } + + subTask.Status = WorkStatus.Blocked; + subTask.BlockingReason = "waiting-for-pad"; + return SubTaskOutcome.Active; + } + + subTask.Status = WorkStatus.Active; + subTask.BlockingReason = null; + ship.AssignedDockingPadIndex = padIndex; + var padPosition = GetDockingPadPosition(station, padIndex.Value); + ship.TargetPosition = padPosition; + if (ship.Position.DistanceTo(padPosition) > 4f) + { + ship.State = ShipState.DockingApproach; + ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return SubTaskOutcome.Active; + } + + ship.State = ShipState.Docking; + if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.DockingDuration)) + { + return SubTaskOutcome.Active; + } + + ship.State = ShipState.Docked; + ship.DockedStationId = station.Id; + station.DockedShipIds.Add(ship.Id); + ship.KnownStationIds.Add(station.Id); + ship.Position = padPosition; + ship.TargetPosition = padPosition; + return SubTaskOutcome.Completed; + } + + private SubTaskOutcome UpdateUndockSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + if (ship.DockedStationId is null) + { + return SubTaskOutcome.Completed; + } + + var station = ResolveStation(world, ship.DockedStationId); + if (station is null) + { + ship.DockedStationId = null; + ship.AssignedDockingPadIndex = null; + return SubTaskOutcome.Completed; + } + + var undockTarget = GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, balance.UndockDistance); + ship.TargetPosition = undockTarget; + ship.State = ShipState.Undocking; + if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.UndockingDuration)) + { + ship.Position = GetShipDockedPosition(ship, station); + return SubTaskOutcome.Active; + } + + ship.Position = ship.Position.MoveToward(undockTarget, balance.UndockDistance); + if (ship.Position.DistanceTo(undockTarget) > MathF.Max(subTask.Threshold, 4f)) + { + return SubTaskOutcome.Active; + } + + station.DockedShipIds.Remove(ship.Id); + ReleaseDockingPad(station, ship.Id); + ship.DockedStationId = null; + ship.AssignedDockingPadIndex = null; + return SubTaskOutcome.Completed; + } + + private SubTaskOutcome UpdateLoadCargoSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + if (ship.DockedStationId is null) + { + subTask.BlockingReason = "not-docked"; + return SubTaskOutcome.Failed; + } + + var station = ResolveStation(world, ship.DockedStationId); + if (station is null) + { + subTask.BlockingReason = "station-missing"; + return SubTaskOutcome.Failed; + } + + ship.TargetPosition = GetShipDockedPosition(ship, station); + ship.Position = ship.TargetPosition; + ship.State = ShipState.Loading; + var itemId = subTask.ItemId; + if (itemId is null) + { + return SubTaskOutcome.Completed; + } + + var desiredAmount = subTask.Amount > 0f ? subTask.Amount : ship.Definition.GetTotalCargoCapacity(); + var availableCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - GetShipCargoAmount(ship)); + var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Trade); + var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(availableCapacity, GetInventoryAmount(station.Inventory, itemId))); + if (moved > 0.01f) + { + RemoveInventory(station.Inventory, itemId, moved); + AddInventory(ship.Inventory, itemId, moved); + } + + var loadedAmount = GetInventoryAmount(ship.Inventory, itemId); + subTask.Progress = desiredAmount <= 0.01f ? 1f : Math.Clamp(loadedAmount / desiredAmount, 0f, 1f); + return availableCapacity <= 0.01f || GetInventoryAmount(station.Inventory, itemId) <= 0.01f || loadedAmount >= desiredAmount - 0.01f + ? SubTaskOutcome.Completed + : SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateUnloadCargoSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + if (ship.DockedStationId is null) + { + subTask.BlockingReason = "not-docked"; + return SubTaskOutcome.Failed; + } + + var station = ResolveStation(world, ship.DockedStationId); + if (station is null) + { + subTask.BlockingReason = "station-missing"; + return SubTaskOutcome.Failed; + } + + ship.TargetPosition = GetShipDockedPosition(ship, station); + ship.Position = ship.TargetPosition; + ship.State = ShipState.Transferring; + var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Mining)); + + if (subTask.ItemId is not null) + { + var moved = MathF.Min(transferRate * deltaSeconds, GetInventoryAmount(ship.Inventory, subTask.ItemId)); + var accepted = TryAddStationInventory(world, station, subTask.ItemId, moved); + RemoveInventory(ship.Inventory, subTask.ItemId, accepted); + subTask.Progress = subTask.Amount <= 0.01f + ? 1f + : Math.Clamp(1f - (GetInventoryAmount(ship.Inventory, subTask.ItemId) / subTask.Amount), 0f, 1f); + return GetInventoryAmount(ship.Inventory, subTask.ItemId) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } + + foreach (var (itemId, amount) in ship.Inventory.ToList().OrderBy(entry => entry.Key, StringComparer.Ordinal)) + { + var moved = MathF.Min(amount, transferRate * deltaSeconds); + var accepted = TryAddStationInventory(world, station, itemId, moved); + RemoveInventory(ship.Inventory, itemId, accepted); + if (accepted > 0.01f) + { + return GetShipCargoAmount(ship) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } + } + + return GetShipCargoAmount(ship) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateTransferCargoToShipSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f); + if (targetShip is null) + { + subTask.BlockingReason = "target-ship-missing"; + return SubTaskOutcome.Failed; + } + + var desiredPosition = GetFormationPosition(targetShip.Position, ship.Id, MathF.Max(subTask.Threshold, 12f)); + subTask.TargetSystemId = targetShip.SystemId; + subTask.TargetPosition = desiredPosition; + if (ship.SystemId != targetShip.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 12f)) + { + return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); + } + + ship.State = ShipState.Transferring; + ship.TargetPosition = desiredPosition; + ship.Position = ship.Position.MoveToward(desiredPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(desiredPosition))); + if (subTask.ItemId is null) + { + return SubTaskOutcome.Completed; + } + + var targetCapacity = MathF.Max(0f, targetShip.Definition.GetTotalCargoCapacity() - GetShipCargoAmount(targetShip)); + if (targetCapacity <= 0.01f) + { + subTask.BlockingReason = "target-cargo-full"; + return SubTaskOutcome.Failed; + } + + var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Navigation)); + var desiredAmount = subTask.Amount > 0f ? subTask.Amount : GetInventoryAmount(ship.Inventory, subTask.ItemId); + var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(targetCapacity, GetInventoryAmount(ship.Inventory, subTask.ItemId))); + if (moved > 0.01f) + { + RemoveInventory(ship.Inventory, subTask.ItemId, moved); + AddInventory(targetShip.Inventory, subTask.ItemId, moved); + } + + var remaining = GetInventoryAmount(ship.Inventory, subTask.ItemId); + subTask.Progress = desiredAmount <= 0.01f ? 1f : Math.Clamp(1f - (remaining / desiredAmount), 0f, 1f); + return remaining <= 0.01f || GetShipCargoAmount(targetShip) >= targetShip.Definition.GetTotalCargoCapacity() - 0.01f + ? SubTaskOutcome.Completed + : SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateSalvageSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.RemainingAmount > 0.01f); + if (wreck is null) + { + return SubTaskOutcome.Completed; + } + + var desiredPosition = subTask.TargetPosition ?? GetFormationPosition(wreck.Position, ship.Id, 8f); + ship.TargetPosition = desiredPosition; + if (ship.SystemId != wreck.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 8f)) + { + subTask.TargetSystemId = wreck.SystemId; + subTask.TargetPosition = desiredPosition; + return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); + } + + ship.State = ShipState.Transferring; + var remainingCapacity = MathF.Max(0f, ship.Definition.GetTotalCargoCapacity() - GetShipCargoAmount(ship)); + if (remainingCapacity <= 0.01f) + { + return SubTaskOutcome.Completed; + } + + if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.4f, balance.MiningCycleSeconds * 0.8f))) + { + return SubTaskOutcome.Active; + } + + var salvageRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Mining, ship.Skills.Trade)); + var recovered = MathF.Min(salvageRate, MathF.Min(remainingCapacity, wreck.RemainingAmount)); + if (recovered > 0.01f) + { + AddInventory(ship.Inventory, wreck.ItemId, recovered); + wreck.RemainingAmount = MathF.Max(0f, wreck.RemainingAmount - recovered); + } + + if (wreck.RemainingAmount <= 0.01f) + { + world.Wrecks.RemoveAll(candidate => candidate.Id == wreck.Id); + } + + subTask.ElapsedSeconds = 0f; + return wreck.RemainingAmount <= 0.01f || GetShipCargoAmount(ship) >= ship.Definition.GetTotalCargoCapacity() - 0.01f + ? SubTaskOutcome.Completed + : SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateDeliverConstructionSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); + var station = site is null ? null : ResolveSupportStation(world, ship, site); + if (site is null || station is null || site.State is ConstructionSiteStateKinds.Completed or ConstructionSiteStateKinds.Destroyed) + { + subTask.BlockingReason = "construction-target-missing"; + return SubTaskOutcome.Failed; + } + + var supportPosition = ResolveSupportPosition(ship, station, site, world); + if (!IsShipWithinSupportRange(ship, supportPosition, MathF.Max(12f, subTask.Threshold))) + { + ship.State = ShipState.LocalFlight; + ship.TargetPosition = supportPosition; + ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return SubTaskOutcome.Active; + } + + ship.TargetPosition = supportPosition; + ship.Position = supportPosition; + ship.State = ShipState.DeliveringConstruction; + var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Construction); + foreach (var required in site.RequiredItems.OrderBy(entry => entry.Key, StringComparer.Ordinal)) + { + var delivered = GetInventoryAmount(site.DeliveredItems, required.Key); + var remaining = MathF.Max(0f, required.Value - delivered); + if (remaining <= 0.01f) + { + continue; + } + + var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key)); + var moved = MathF.Min(remaining, MathF.Min(available, transferRate * deltaSeconds)); + if (moved <= 0.01f) + { + continue; + } + + RemoveInventory(station.Inventory, required.Key, moved); + AddInventory(site.Inventory, required.Key, moved); + AddInventory(site.DeliveredItems, required.Key, moved); + break; + } + + subTask.Progress = site.RequiredItems.Count == 0 + ? 1f + : site.RequiredItems.Sum(required => + required.Value <= 0.01f + ? 1f + : Math.Clamp(GetInventoryAmount(site.DeliveredItems, required.Key) / required.Value, 0f, 1f)) / site.RequiredItems.Count; + return IsConstructionSiteReady(world, site) ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateBuildConstructionSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) + { + var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); + var station = site is null ? null : ResolveSupportStation(world, ship, site); + if (site is null || station is null || site.BlueprintId is null || site.State is ConstructionSiteStateKinds.Completed or ConstructionSiteStateKinds.Destroyed) + { + subTask.BlockingReason = "construction-site-missing"; + return SubTaskOutcome.Failed; + } + + var supportPosition = ResolveSupportPosition(ship, station, site, world); + if (!IsShipWithinSupportRange(ship, supportPosition, MathF.Max(12f, subTask.Threshold))) + { + ship.State = ShipState.LocalFlight; + ship.TargetPosition = supportPosition; + ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return SubTaskOutcome.Active; + } + + if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)) + { + ship.State = ShipState.WaitingMaterials; + subTask.Status = WorkStatus.Blocked; + subTask.BlockingReason = "waiting-materials"; + return SubTaskOutcome.Active; + } + + subTask.Status = WorkStatus.Active; + subTask.BlockingReason = null; + ship.TargetPosition = supportPosition; + ship.Position = supportPosition; + ship.State = ShipState.Constructing; + site.AssignedConstructorShipIds.Add(ship.Id); + site.Progress += deltaSeconds * GetSkillFactor(ship.Skills.Construction); + subTask.Progress = recipe.Duration <= 0.01f ? 1f : Math.Clamp(site.Progress / recipe.Duration, 0f, 1f); + if (site.Progress < recipe.Duration) + { + return SubTaskOutcome.Active; + } + + if (site.StationId is null) + { + CompleteStationFoundation(world, station, site); + } + else + { + AddStationModule(world, station, site.BlueprintId); + PrepareNextConstructionSiteStep(world, station, site); + } + + site.State = ConstructionSiteStateKinds.Completed; + return SubTaskOutcome.Completed; + } + + private static bool AdvanceTimedSubTask(ShipSubTaskRuntime subTask, float deltaSeconds, float requiredSeconds) + { + subTask.TotalSeconds = requiredSeconds; + subTask.ElapsedSeconds += deltaSeconds; + subTask.Progress = requiredSeconds <= 0.01f ? 1f : Math.Clamp(subTask.ElapsedSeconds / requiredSeconds, 0f, 1f); + if (subTask.ElapsedSeconds < requiredSeconds) + { + return false; + } + + subTask.ElapsedSeconds = 0f; + return true; + } + + private SubTaskOutcome UpdateLocalTravel( + SimulationWorld world, + ShipRuntime ship, + ShipSubTaskRuntime subTask, + float deltaSeconds, + string targetSystemId, + Vector3 targetPosition, + CelestialRuntime? targetCelestial, + bool completeOnArrival) + { + var distance = ship.Position.DistanceTo(targetPosition); + ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; + ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; + ship.SpatialState.Transit = null; + ship.SpatialState.DestinationNodeId = targetCelestial?.Id; + subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f); + + if (distance <= MathF.Max(subTask.Threshold, balance.ArrivalThreshold)) + { + ship.Position = targetPosition; + ship.TargetPosition = targetPosition; + ship.SystemId = targetSystemId; + ship.SpatialState.CurrentSystemId = targetSystemId; + ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; + ship.State = ShipState.Arriving; + return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } + + ship.State = ShipState.LocalFlight; + ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); + return SubTaskOutcome.Active; + } + + private SubTaskOutcome UpdateWarpTransit( + SimulationWorld world, + ShipRuntime ship, + ShipSubTaskRuntime subTask, + float deltaSeconds, + Vector3 targetPosition, + CelestialRuntime targetCelestial, + bool completeOnArrival) + { + var transit = ship.SpatialState.Transit; + if (transit is null || transit.Regime != MovementRegimeKind.Warp || transit.DestinationNodeId != targetCelestial.Id) + { + transit = new ShipTransitRuntime + { + Regime = MovementRegimeKind.Warp, + OriginNodeId = ship.SpatialState.CurrentCelestialId, + DestinationNodeId = targetCelestial.Id, + StartedAtUtc = world.GeneratedAtUtc, + }; + ship.SpatialState.Transit = transit; + subTask.ElapsedSeconds = 0f; + } + + ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace; + ship.SpatialState.MovementRegime = MovementRegimeKind.Warp; + ship.SpatialState.CurrentCelestialId = null; + ship.SpatialState.DestinationNodeId = targetCelestial.Id; + + var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f); + if (ship.State != ShipState.Warping) + { + ship.State = ShipState.SpoolingWarp; + if (!AdvanceTimedSubTask(subTask, deltaSeconds, spoolDuration)) + { + return SubTaskOutcome.Active; + } + + ship.State = ShipState.Warping; + } + + var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null + ? ship.Position.DistanceTo(targetPosition) + : (world.Celestials.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition))); + ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds); + transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance)); + subTask.Progress = transit.Progress; + if (ship.Position.DistanceTo(targetPosition) > 18f) + { + return SubTaskOutcome.Active; + } + + return CompleteTransitArrival(ship, subTask.TargetSystemId ?? ship.SystemId, targetPosition, targetCelestial, completeOnArrival); + } + + private SubTaskOutcome UpdateFtlTransit( + SimulationWorld world, + ShipRuntime ship, + ShipSubTaskRuntime subTask, + float deltaSeconds, + string targetSystemId, + Vector3 entryPosition, + CelestialRuntime? targetCelestial, + bool completeOnArrival, + Vector3 finalTargetPosition) + { + var destinationNodeId = targetCelestial?.Id; + var transit = ship.SpatialState.Transit; + if (transit is null || transit.Regime != MovementRegimeKind.FtlTransit || transit.DestinationNodeId != destinationNodeId) + { + transit = new ShipTransitRuntime + { + Regime = MovementRegimeKind.FtlTransit, + OriginNodeId = ship.SpatialState.CurrentCelestialId, + DestinationNodeId = destinationNodeId, + StartedAtUtc = world.GeneratedAtUtc, + }; + ship.SpatialState.Transit = transit; + subTask.ElapsedSeconds = 0f; + } + + ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace; + ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit; + ship.SpatialState.CurrentCelestialId = null; + ship.SpatialState.DestinationNodeId = destinationNodeId; + + if (ship.State != ShipState.Ftl) + { + ship.State = ShipState.SpoolingFtl; + if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(ship.Definition.SpoolTime, 0.1f))) + { + return SubTaskOutcome.Active; + } + + ship.State = ShipState.Ftl; + } + + var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId); + var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId); + var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition)); + transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * GetSkillFactor(ship.Skills.Navigation)) * deltaSeconds / totalDistance)); + subTask.Progress = transit.Progress; + if (transit.Progress < 0.999f) + { + return SubTaskOutcome.Active; + } + + ship.Position = entryPosition; + ship.TargetPosition = finalTargetPosition; + ship.SystemId = targetSystemId; + ship.SpatialState.CurrentSystemId = targetSystemId; + ship.SpatialState.Transit = null; + ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; + ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; + ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; + ship.SpatialState.DestinationNodeId = targetCelestial?.Id; + ship.State = ShipState.Arriving; + + // Cross-system travel is only complete once the ship finishes the + // destination-system local leg to the actual target. + return SubTaskOutcome.Active; + } + + private static SubTaskOutcome CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial, bool completeOnArrival) + { + ship.Position = targetPosition; + ship.TargetPosition = targetPosition; + ship.SystemId = targetSystemId; + ship.SpatialState.CurrentSystemId = targetSystemId; + ship.SpatialState.Transit = null; + ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; + ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; + ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; + ship.SpatialState.DestinationNodeId = targetCelestial?.Id; + ship.State = ShipState.Arriving; + return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active; + } +} diff --git a/apps/backend/Ships/AI/ShipAiService.Helpers.cs b/apps/backend/Ships/AI/ShipAiService.Helpers.cs new file mode 100644 index 0000000..d30283d --- /dev/null +++ b/apps/backend/Ships/AI/ShipAiService.Helpers.cs @@ -0,0 +1,947 @@ +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; +using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; +using static SpaceGame.Api.Stations.Simulation.StationSimulationService; + +namespace SpaceGame.Api.Ships.AI; + +public sealed partial class ShipAiService +{ + private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ShipSubTaskRuntime subTask) + { + if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId)) + { + var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); + if (ship is not null) + { + return ship.Position; + } + + var station = ResolveStation(world, subTask.TargetEntityId); + if (station is not null) + { + return station.Position; + } + + var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); + if (celestial is not null) + { + return celestial.Position; + } + + var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); + if (wreck is not null) + { + return wreck.Position; + } + } + + return subTask.TargetPosition ?? Vector3.Zero; + } + + private static CelestialRuntime? ResolveTravelTargetCelestial(SimulationWorld world, ShipSubTaskRuntime subTask, Vector3 targetPosition) + { + if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId)) + { + var station = ResolveStation(world, subTask.TargetEntityId); + if (station?.CelestialId is not null) + { + return world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId); + } + + var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); + if (site?.CelestialId is not null) + { + return world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId); + } + + var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); + if (celestial is not null) + { + return celestial; + } + + if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) is { } wreck) + { + return world.Celestials + .Where(candidate => candidate.SystemId == wreck.SystemId) + .OrderBy(candidate => candidate.Position.DistanceTo(wreck.Position)) + .FirstOrDefault(); + } + } + + return world.Celestials + .Where(candidate => subTask.TargetSystemId is null || candidate.SystemId == subTask.TargetSystemId) + .OrderBy(candidate => candidate.Position.DistanceTo(targetPosition)) + .FirstOrDefault(); + } + + private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship) + { + if (ship.SpatialState.CurrentCelestialId is not null) + { + return world.Celestials.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentCelestialId); + } + + return world.Celestials + .Where(candidate => candidate.SystemId == ship.SystemId) + .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) + .FirstOrDefault(); + } + + private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) => + world.Celestials.FirstOrDefault(candidate => candidate.SystemId == systemId && candidate.Kind == SpatialNodeKind.Star); + + private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) => + world.Systems.FirstOrDefault(candidate => candidate.Definition.Id == systemId)?.Position ?? Vector3.Zero; + + private static float GetLocalTravelSpeed(ShipRuntime ship) => + SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed) * GetSkillFactor(ship.Skills.Navigation); + + private static float GetWarpTravelSpeed(ShipRuntime ship) => + SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed) * GetSkillFactor(ship.Skills.Navigation); + + private static float GetSkillFactor(int skillLevel) => + Math.Clamp(1f + ((skillLevel - 3) * 0.08f), 0.75f, 1.4f); + + private static int GetEffectiveSkillLevel( + SimulationWorld world, + ShipRuntime ship, + Func captainSelector, + Func managerSelector) + { + var captainLevel = captainSelector(ship.Skills); + if (ship.CommanderId is null) + { + return captainLevel; + } + + var shipCommander = world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId); + var manager = shipCommander?.ParentCommanderId is null + ? shipCommander + : world.Commanders.FirstOrDefault(candidate => candidate.Id == shipCommander.ParentCommanderId) ?? shipCommander; + return Math.Clamp((captainLevel + (manager is null ? 3 : managerSelector(manager.Skills)) + 1) / 2, 1, 5); + } + + private static int ResolveBehaviorSystemRange(SimulationWorld world, ShipRuntime ship, string behaviorKind, int explicitRange) + { + if (explicitRange > 0) + { + return explicitRange; + } + + var tradeSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Trade, skills => skills.Coordination); + var miningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination); + var combatSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Combat, skills => skills.Strategy); + return behaviorKind switch + { + LocalAutoMine or LocalAutoTrade => 0, + AdvancedAutoMine => Math.Clamp(1 + ((miningSkill - 1) / 2), 1, 3), + AdvancedAutoTrade => Math.Clamp(1 + ((tradeSkill - 1) / 2), 1, 3), + ExpertAutoMine => Math.Clamp(2 + ((miningSkill - 1) / 2), 2, Math.Max(world.Systems.Count - 1, 2)), + FillShortages or FindBuildTasks or RevisitKnownStations or SupplyFleet => Math.Clamp(1 + ((tradeSkill + 1) / 2), 1, Math.Max(world.Systems.Count - 1, 1)), + Patrol or Police or ProtectPosition or ProtectShip or ProtectStation => Math.Clamp(1 + ((combatSkill - 1) / 2), 1, Math.Max(world.Systems.Count - 1, 1)), + _ => Math.Max(world.Systems.Count - 1, 0), + }; + } + + private static int GetSystemDistanceTier(SimulationWorld world, string originSystemId, string targetSystemId) + { + if (string.Equals(originSystemId, targetSystemId, StringComparison.Ordinal)) + { + return 0; + } + + var originPosition = ResolveSystemGalaxyPosition(world, originSystemId); + return world.Systems + .OrderBy(system => system.Position.DistanceTo(originPosition)) + .ThenBy(system => system.Definition.Id, StringComparer.Ordinal) + .Select(system => system.Definition.Id) + .TakeWhile(systemId => !string.Equals(systemId, targetSystemId, StringComparison.Ordinal)) + .Count(); + } + + private static bool IsWithinSystemRange(SimulationWorld world, string originSystemId, string targetSystemId, int maxRange) => + maxRange < 0 || GetSystemDistanceTier(world, originSystemId, targetSystemId) <= maxRange; + + private static float GetShipDamagePerSecond(ShipRuntime ship) => + ship.Definition.Type switch + { + ShipType.Frigate => FrigateDps, + ShipType.Destroyer => DestroyerDps, + ShipType.Battleship => CruiserDps, + ShipType.Carrier => CapitalDps, + _ => 4f, + }; + + private static MiningOpportunity? SelectMiningOpportunity( + SimulationWorld world, + ShipRuntime ship, + StationRuntime homeStation, + CommanderAssignmentRuntime? assignment, + string behaviorKind) + { + var policy = ResolvePolicy(world, ship.PolicySetId); + var preferredItemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId; + var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange); + var effectiveMiningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination); + string? deniedReason = null; + var opportunity = world.Nodes + .Where(node => + { + if (node.OreRemaining <= 0.01f || !CanExtractNode(ship, node, world) || (preferredItemId is not null && !string.Equals(node.ItemId, preferredItemId, StringComparison.Ordinal))) + { + return false; + } + + if (!TryCheckSystemAllowed(world, policy, ship.FactionId, node.SystemId, "military", out var reason)) + { + deniedReason ??= reason; + return false; + } + + return IsWithinSystemRange(world, homeStation.SystemId, node.SystemId, rangeBudget); + }) + .Select(node => + { + var buyer = SelectBestDeliveryStation(world, ship, node.ItemId, homeStation, behaviorKind); + var demandScore = GetFactionDemandScore(world, ship.FactionId, node.ItemId); + var distancePenalty = GetSystemDistanceTier(world, homeStation.SystemId, node.SystemId) * 18f; + var routeRiskPenalty = GeopoliticalSimulationService.GetSystemRouteRisk(world, node.SystemId, ship.FactionId) * 30f; + var score = (node.SystemId == homeStation.SystemId ? 55f : 0f) + + (node.OreRemaining * 0.025f) + + (demandScore * (string.Equals(behaviorKind, ExpertAutoMine, StringComparison.Ordinal) ? 22f : 12f)) + + (effectiveMiningSkill * 10f) + - distancePenalty + - routeRiskPenalty + - node.Position.DistanceTo(ship.Position); + return new MiningOpportunity(node, buyer, score, $"Mine {node.ItemId} for {buyer.Label}"); + }) + .OrderByDescending(candidate => candidate.Score) + .ThenBy(candidate => candidate.Node.Id, StringComparer.Ordinal) + .FirstOrDefault(); + if (opportunity is null && deniedReason is not null) + { + ship.LastAccessFailureReason = deniedReason; + } + + return opportunity; + } + + private static TradeRoutePlan? SelectTradeRoute( + SimulationWorld world, + ShipRuntime ship, + StationRuntime? homeStation, + string behaviorKind, + bool knownStationsOnly) + { + var policy = ResolvePolicy(world, ship.PolicySetId); + var stationsById = world.Stations + .Where(station => station.FactionId == ship.FactionId) + .ToDictionary(station => station.Id, StringComparer.Ordinal); + var originSystemId = homeStation?.SystemId ?? ship.SystemId; + var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange); + var effectiveTradeSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Trade, skills => skills.Coordination); + var requireKnownStations = knownStationsOnly || string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal); + string? deniedReason = null; + + var route = world.MarketOrders + .Where(order => + order.FactionId == ship.FactionId && + order.Kind == MarketOrderKinds.Buy && + order.RemainingAmount > 0.01f) + .Select(order => + { + StationRuntime? destination = null; + ConstructionSiteRuntime? destinationSite = null; + if (order.StationId is not null && stationsById.TryGetValue(order.StationId, out var destinationStation)) + { + destination = destinationStation; + } + else if (order.ConstructionSiteId is not null) + { + destinationSite = world.ConstructionSites.FirstOrDefault(site => site.Id == order.ConstructionSiteId); + if (destinationSite is not null) + { + destination = ResolveSupportStation(world, ship, destinationSite); + } + } + + if (destination is null) + { + return null; + } + + if (!TryCheckSystemAllowed(world, policy, ship.FactionId, destination.SystemId, "trade", out var destinationDeniedReason)) + { + deniedReason ??= destinationDeniedReason; + return null; + } + if (!IsWithinSystemRange(world, originSystemId, destination.SystemId, rangeBudget)) + { + return null; + } + if (requireKnownStations + && ship.KnownStationIds.Count > 0 + && !ship.KnownStationIds.Contains(destination.Id) + && (homeStation is null || !string.Equals(destination.Id, homeStation.Id, StringComparison.Ordinal))) + { + return null; + } + if (string.Equals(behaviorKind, FindBuildTasks, StringComparison.Ordinal) && destinationSite is null) + { + return null; + } + if (!string.Equals(behaviorKind, FindBuildTasks, StringComparison.Ordinal) && destinationSite is not null) + { + return null; + } + + var source = stationsById.Values + .Where(station => + { + if (station.Id == destination.Id || GetInventoryAmount(station.Inventory, order.ItemId) <= GetStationReserveFloor(world, station, order.ItemId) + 1f) + { + return false; + } + + if (!TryCheckSystemAllowed(world, policy, ship.FactionId, station.SystemId, "trade", out var sourceDeniedReason)) + { + deniedReason ??= sourceDeniedReason; + return false; + } + + if (!IsWithinSystemRange(world, originSystemId, station.SystemId, rangeBudget)) + { + return false; + } + + return !requireKnownStations + || ship.KnownStationIds.Count == 0 + || ship.KnownStationIds.Contains(station.Id) + || (homeStation is not null && string.Equals(station.Id, homeStation.Id, StringComparison.Ordinal)); + }) + .OrderByDescending(station => GetInventoryAmount(station.Inventory, order.ItemId) - GetStationReserveFloor(world, station, order.ItemId)) + .ThenByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0) + .ThenBy(station => station.Id, StringComparer.Ordinal) + .FirstOrDefault(); + if (source is null) + { + return null; + } + + var shortageBias = string.Equals(behaviorKind, FillShortages, StringComparison.Ordinal) + ? GetFactionDemandScore(world, ship.FactionId, order.ItemId) * 35f + : 0f; + var buildBias = destinationSite is null ? 0f : 65f; + var revisitBias = string.Equals(behaviorKind, RevisitKnownStations, StringComparison.Ordinal) && ship.KnownStationIds.Contains(source.Id) && ship.KnownStationIds.Contains(destination.Id) + ? 28f + : 0f; + var regionalNeedBias = GetRegionalCommodityPressure(world, ship.FactionId, destination.SystemId, order.ItemId) * 18f; + var systemRangePenalty = (GetSystemDistanceTier(world, originSystemId, source.SystemId) + GetSystemDistanceTier(world, originSystemId, destination.SystemId)) * 16f; + var riskPenalty = + (GeopoliticalSimulationService.GetSystemRouteRisk(world, source.SystemId, ship.FactionId) + + GeopoliticalSimulationService.GetSystemRouteRisk(world, destination.SystemId, ship.FactionId)) * 22f; + var distanceScore = source.Position.DistanceTo(ship.Position) + source.Position.DistanceTo(destination.Position); + var score = (order.Valuation * 50f) + + shortageBias + + buildBias + + revisitBias + + regionalNeedBias + + (effectiveTradeSkill * 12f) + - systemRangePenalty + - riskPenalty + - distanceScore; + var summary = destinationSite is null + ? $"{order.ItemId}: {source.Label} -> {destination.Label}" + : $"{order.ItemId}: {source.Label} -> build support {destination.Label}"; + return new TradeRoutePlan(source, destination, order.ItemId, score, summary); + }) + .Where(route => route is not null) + .Cast() + .OrderByDescending(route => route.Score) + .ThenBy(route => route.ItemId, StringComparer.Ordinal) + .ThenBy(route => route.SourceStation.Id, StringComparer.Ordinal) + .FirstOrDefault(); + if (route is null && deniedReason is not null) + { + ship.LastAccessFailureReason = deniedReason; + } + + return route; + } + + private static FleetSupplyPlan? SelectFleetSupplyPlan(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation) + { + var assignment = ResolveAssignment(world, ship); + var targetCandidates = world.Ships + .Where(candidate => + candidate.Id != ship.Id && + candidate.FactionId == ship.FactionId && + candidate.Definition.GetTotalCargoCapacity() > 0.01f && + (assignment?.TargetEntityId is null || string.Equals(candidate.Id, assignment.TargetEntityId, StringComparison.Ordinal))) + .OrderByDescending(candidate => IsMilitaryShip(candidate.Definition) ? 1 : 0) + .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) + .ToList(); + if (targetCandidates.Count == 0) + { + return null; + } + + var sourceStations = world.Stations + .Where(station => station.FactionId == ship.FactionId) + .OrderByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0) + .ThenBy(station => station.Id, StringComparer.Ordinal) + .ToList(); + foreach (var target in targetCandidates) + { + var itemId = assignment?.ItemId + ?? sourceStations + .SelectMany(station => station.Inventory) + .Where(entry => entry.Value > 2f) + .OrderByDescending(entry => entry.Value) + .ThenBy(entry => entry.Key, StringComparer.Ordinal) + .Select(entry => entry.Key) + .FirstOrDefault(); + if (itemId is null) + { + continue; + } + + var source = sourceStations.FirstOrDefault(station => GetInventoryAmount(station.Inventory, itemId) > 2f); + if (source is null) + { + continue; + } + + var amount = MathF.Min(MathF.Max(10f, ship.Definition.GetTotalCargoCapacity() * 0.5f), GetInventoryAmount(source.Inventory, itemId)); + return new FleetSupplyPlan(source, target, itemId, amount, MathF.Max(16f, ship.DefaultBehavior.Radius), $"Supply {target.Definition.Name} with {itemId}"); + } + + return null; + } + + private static StationRuntime? SelectKnownStationVisit(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation) + { + var candidateIds = ship.KnownStationIds.Count == 0 && homeStation is not null + ? [homeStation.Id] + : ship.KnownStationIds.OrderBy(id => id, StringComparer.Ordinal).ToArray(); + return candidateIds + .Select(id => ResolveStation(world, id)) + .Where(station => station is not null && station.FactionId == ship.FactionId) + .Cast() + .OrderByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0) + .ThenBy(station => station.SystemId == ship.SystemId ? 0 : 1) + .ThenBy(station => station.Position.DistanceTo(ship.Position)) + .FirstOrDefault(); + } + + private static StationRuntime SelectBestDeliveryStation(SimulationWorld world, ShipRuntime ship, string itemId, StationRuntime homeStation, string behaviorKind) + { + if (!string.Equals(behaviorKind, ExpertAutoMine, StringComparison.Ordinal)) + { + return homeStation; + } + + return world.Stations + .Where(station => station.FactionId == ship.FactionId) + .OrderByDescending(station => GetFactionDemandScore(world, ship.FactionId, itemId) + GetRegionalCommodityPressure(world, ship.FactionId, station.SystemId, itemId) + (station.Id == homeStation.Id ? 5f : 0f)) + .ThenBy(station => station.SystemId == homeStation.SystemId ? 0 : 1) + .ThenBy(station => station.Id, StringComparer.Ordinal) + .FirstOrDefault() + ?? homeStation; + } + + private static ResourceNodeRuntime? SelectLocalMiningNode(SimulationWorld world, ShipRuntime ship, string systemId, string itemId) + { + var policy = ResolvePolicy(world, ship.PolicySetId); + string? deniedReason = null; + var node = world.Nodes + .Where(candidate => + { + if (!string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal) + || !string.Equals(candidate.ItemId, itemId, StringComparison.Ordinal) + || candidate.OreRemaining <= 0.01f + || !CanExtractNode(ship, candidate, world)) + { + return false; + } + + if (!TryCheckSystemAllowed(world, policy, ship.FactionId, candidate.SystemId, "military", out var reason)) + { + deniedReason ??= reason; + return false; + } + + return true; + }) + .OrderByDescending(candidate => candidate.OreRemaining) + .ThenBy(candidate => candidate.Position.DistanceTo(ship.Position)) + .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) + .FirstOrDefault(); + if (node is null && deniedReason is not null) + { + ship.LastAccessFailureReason = deniedReason; + } + + return node; + } + + private static StationRuntime? SelectLocalAutoMineBuyer(SimulationWorld world, ShipRuntime ship, string systemId, string itemId) + { + var policy = ResolvePolicy(world, ship.PolicySetId); + var stationsById = world.Stations.ToDictionary(station => station.Id, StringComparer.Ordinal); + string? deniedReason = null; + var buyer = world.MarketOrders + .Where(order => + order.Kind == MarketOrderKinds.Buy + && string.Equals(order.ItemId, itemId, StringComparison.Ordinal) + && order.RemainingAmount > 0.01f) + .Select(order => + { + StationRuntime? destination = null; + if (order.StationId is not null && stationsById.TryGetValue(order.StationId, out var station)) + { + destination = station; + } + else if (order.ConstructionSiteId is not null) + { + var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == order.ConstructionSiteId); + if (site is not null) + { + destination = ResolveSupportStation(world, ship, site); + } + } + + if (destination is null || !string.Equals(destination.SystemId, systemId, StringComparison.Ordinal)) + { + return null; + } + + if (!TryCheckSystemAllowed(world, policy, ship.FactionId, destination.SystemId, "trade", out var reason)) + { + deniedReason ??= reason; + return null; + } + + var score = (order.Valuation * 20f) + + MathF.Min(order.RemainingAmount, ship.Definition.GetTotalCargoCapacity()) + - destination.Position.DistanceTo(ship.Position); + return new LocalMiningBuyerCandidate(destination, score); + }) + .Where(candidate => candidate is not null) + .Cast() + .OrderByDescending(candidate => candidate.Score) + .ThenBy(candidate => candidate.Station.Id, StringComparer.Ordinal) + .Select(candidate => candidate.Station) + .FirstOrDefault(); + if (buyer is null && deniedReason is not null) + { + ship.LastAccessFailureReason = deniedReason; + } + + return buyer; + } + + private static float GetFactionDemandScore(SimulationWorld world, string factionId, string itemId) + { + var signal = CommanderPlanningService.FindFactionEconomicAssessment(world, factionId)? + .CommoditySignals + .FirstOrDefault(candidate => candidate.ItemId == itemId); + var regionalBottleneckScore = world.Geopolitics?.EconomyRegions.Bottlenecks + .Where(bottleneck => string.Equals(bottleneck.ItemId, itemId, StringComparison.Ordinal)) + .Join( + world.Geopolitics.EconomyRegions.Regions.Where(region => string.Equals(region.FactionId, factionId, StringComparison.Ordinal)), + bottleneck => bottleneck.RegionId, + region => region.Id, + (bottleneck, _) => bottleneck.Severity) + .DefaultIfEmpty() + .Max() ?? 0f; + if (signal is null) + { + return regionalBottleneckScore * 8f; + } + + return MathF.Max(0f, signal.BuyBacklog + signal.ReservedForConstruction + MathF.Max(0f, -signal.ProjectedNetRatePerSecond * 50f) + (regionalBottleneckScore * 8f)); + } + + private static float GetRegionalCommodityPressure(SimulationWorld world, string factionId, string systemId, string itemId) + { + var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, systemId); + if (region is null) + { + return 0f; + } + + var bottleneck = world.Geopolitics?.EconomyRegions.Bottlenecks + .FirstOrDefault(candidate => string.Equals(candidate.RegionId, region.Id, StringComparison.Ordinal) + && string.Equals(candidate.ItemId, itemId, StringComparison.Ordinal)); + var assessment = world.Geopolitics?.EconomyRegions.EconomicAssessments + .FirstOrDefault(candidate => string.Equals(candidate.RegionId, region.Id, StringComparison.Ordinal)); + return (bottleneck?.Severity ?? 0f) + ((assessment?.ConstructionPressure ?? 0f) * 2f); + } + + private static ThreatTargetCandidate? SelectThreatTarget( + SimulationWorld world, + ShipRuntime ship, + string targetSystemId, + Vector3 anchorPosition, + float radius, + string? excludeEntityId = null) + { + var policy = ResolvePolicy(world, ship.PolicySetId); + return world.Ships + .Where(candidate => + candidate.Id != excludeEntityId && + candidate.Health > 0f && + candidate.FactionId != ship.FactionId && + string.Equals(candidate.SystemId, targetSystemId, StringComparison.Ordinal) && + candidate.Position.DistanceTo(anchorPosition) <= radius * 1.75f) + .Select(candidate => new ThreatTargetCandidate( + candidate.Id, + candidate.SystemId, + candidate.Position, + 100f + + (IsMilitaryShip(candidate.Definition) ? 30f : 0f) + - candidate.Position.DistanceTo(anchorPosition) + - candidate.Position.DistanceTo(ship.Position) + + (string.Equals(policy?.CombatEngagementPolicy, "aggressive", StringComparison.OrdinalIgnoreCase) ? 12f : 0f))) + .Concat(world.Stations + .Where(candidate => + candidate.Id != excludeEntityId && + candidate.FactionId != ship.FactionId && + string.Equals(candidate.SystemId, targetSystemId, StringComparison.Ordinal) && + candidate.Position.DistanceTo(anchorPosition) <= radius * 2f) + .Select(candidate => new ThreatTargetCandidate(candidate.Id, candidate.SystemId, candidate.Position, 45f - candidate.Position.DistanceTo(anchorPosition) * 0.2f))) + .OrderByDescending(candidate => candidate.Score) + .ThenBy(candidate => candidate.EntityId, StringComparer.Ordinal) + .FirstOrDefault(); + } + + private static PoliceContactCandidate? SelectPoliceContact(SimulationWorld world, ShipRuntime ship, string systemId, Vector3 anchorPosition, float radius) + { + var policy = ResolvePolicy(world, ship.PolicySetId); + return world.Ships + .Where(candidate => + candidate.Id != ship.Id && + candidate.Health > 0f && + candidate.FactionId != ship.FactionId && + string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal) && + candidate.Position.DistanceTo(anchorPosition) <= radius * 1.5f) + .Select(candidate => + { + var engage = IsMilitaryShip(candidate.Definition) + || string.Equals(policy?.CombatEngagementPolicy, "aggressive", StringComparison.OrdinalIgnoreCase); + var score = (engage ? 80f : 40f) + - candidate.Position.DistanceTo(anchorPosition) + - candidate.Position.DistanceTo(ship.Position) + + (IsTransportShip(candidate.Definition) ? 8f : 0f); + return new PoliceContactCandidate(candidate.Id, candidate.SystemId, candidate.Position, engage, score); + }) + .OrderByDescending(candidate => candidate.Score) + .ThenBy(candidate => candidate.EntityId, StringComparer.Ordinal) + .FirstOrDefault(); + } + + private static SalvageOpportunity? SelectSalvageOpportunity(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation) + { + if (homeStation is null) + { + return null; + } + + var rangeBudget = ResolveBehaviorSystemRange(world, ship, AutoSalvage, ship.DefaultBehavior.MaxSystemRange > 0 ? ship.DefaultBehavior.MaxSystemRange : 1); + return world.Wrecks + .Where(wreck => + wreck.RemainingAmount > 0.01f && + IsWithinSystemRange(world, homeStation.SystemId, wreck.SystemId, rangeBudget)) + .Select(wreck => new SalvageOpportunity( + wreck, + (wreck.RemainingAmount * 3f) - wreck.Position.DistanceTo(ship.Position) - (GetSystemDistanceTier(world, homeStation.SystemId, wreck.SystemId) * 25f), + $"Salvage {wreck.ItemId} from {wreck.SourceEntityId}")) + .OrderByDescending(candidate => candidate.Score) + .ThenBy(candidate => candidate.Wreck.Id, StringComparer.Ordinal) + .FirstOrDefault(); + } + + private static (string SystemId, Vector3 Position)? ResolveObjectTarget(SimulationWorld world, string? entityId) + { + if (entityId is null) + { + return null; + } + + if (world.Ships.FirstOrDefault(candidate => candidate.Id == entityId) is { } ship) + { + return (ship.SystemId, ship.Position); + } + + if (ResolveStation(world, entityId) is { } station) + { + return (station.SystemId, station.Position); + } + + if (world.Celestials.FirstOrDefault(candidate => candidate.Id == entityId) is { } celestial) + { + return (celestial.SystemId, celestial.Position); + } + + if (world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == entityId) is { } site) + { + var position = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? Vector3.Zero; + return (site.SystemId, position); + } + + if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == entityId) is { } wreck) + { + return (wreck.SystemId, wreck.Position); + } + + return null; + } + + private static Vector3 GetFormationPosition(Vector3 anchorPosition, string seed, float radius) + { + var hash = Math.Abs(seed.Aggregate(17, (acc, c) => (acc * 31) + c)); + var angle = (hash % 360) * (MathF.PI / 180f); + return new Vector3( + anchorPosition.X + (MathF.Cos(angle) * radius), + anchorPosition.Y, + anchorPosition.Z + (MathF.Sin(angle) * radius)); + } + + private static TradeRoutePlan? ResolveTradeRoute(SimulationWorld world, string itemId, string sourceStationId, string destinationStationId) + { + var source = ResolveStation(world, sourceStationId); + var destination = ResolveStation(world, destinationStationId); + return source is null || destination is null ? null : new TradeRoutePlan(source, destination, itemId, 0f, $"{itemId}: {source.Label} -> {destination.Label}"); + } + + private static StationRuntime? ResolveStation(SimulationWorld world, string? stationId) => + stationId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == stationId); + + private static ResourceNodeRuntime? ResolveNode(SimulationWorld world, string? nodeId) => + nodeId is null ? null : world.Nodes.FirstOrDefault(candidate => candidate.Id == nodeId); + + private static PolicySetRuntime? ResolvePolicy(SimulationWorld world, string? policySetId) => + policySetId is null ? null : world.Policies.FirstOrDefault(policy => policy.Id == policySetId); + + private static bool IsSystemAllowed( + SimulationWorld world, + PolicySetRuntime? policy, + string factionId, + string systemId, + string accessKind) => + TryCheckSystemAllowed(world, policy, factionId, systemId, accessKind, out _); + + private static bool TryCheckSystemAllowed( + SimulationWorld world, + PolicySetRuntime? policy, + string factionId, + string systemId, + string accessKind, + out string? denialReason) + { + denialReason = null; + if (policy?.BlacklistedSystemIds.Contains(systemId) == true) + { + denialReason = $"blacklisted:{systemId}"; + return false; + } + + var controlState = GeopoliticalSimulationService.GetSystemControlState(world, systemId); + var authorityFactionId = controlState?.ControllerFactionId ?? controlState?.PrimaryClaimantFactionId; + if (authorityFactionId is null || string.Equals(authorityFactionId, factionId, StringComparison.Ordinal)) + { + return true; + } + + var hasAccess = string.Equals(accessKind, "trade", StringComparison.Ordinal) + ? GeopoliticalSimulationService.HasTradeAccess(world, factionId, authorityFactionId) + : GeopoliticalSimulationService.HasMilitaryAccess(world, factionId, authorityFactionId); + if (!hasAccess) + { + denialReason = $"{accessKind}-access-denied:{authorityFactionId}"; + return false; + } + + if (policy?.AvoidHostileSystems != true) + { + return true; + } + + if (GeopoliticalSimulationService.HasHostileRelation(world, factionId, authorityFactionId)) + { + denialReason = $"hostile-authority:{authorityFactionId}"; + return false; + } + + var hostileInfluencer = controlState?.InfluencingFactionIds.FirstOrDefault(candidate => + !string.Equals(candidate, factionId, StringComparison.Ordinal) + && GeopoliticalSimulationService.HasHostileRelation(world, factionId, candidate)); + if (hostileInfluencer is not null) + { + denialReason = $"hostile-influence:{hostileInfluencer}"; + return false; + } + + return true; + } + + private static CommanderAssignmentRuntime? ResolveAssignment(SimulationWorld world, ShipRuntime ship) => + ship.CommanderId is null + ? null + : world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId)?.Assignment; + + private static ShipPlanStepRuntime? GetCurrentStep(ShipPlanRuntime? plan) => + plan is null || plan.CurrentStepIndex >= plan.Steps.Count ? null : plan.Steps[plan.CurrentStepIndex]; + + private static StationRuntime? ResolveSupportStation(SimulationWorld world, ShipRuntime ship, ConstructionSiteRuntime site) + { + return ResolveStation(world, ResolveAssignment(world, ship)?.HomeStationId ?? ship.DefaultBehavior.HomeStationId) + ?? world.Stations + .Where(station => station.FactionId == ship.FactionId) + .OrderByDescending(station => station.SystemId == site.SystemId ? 1 : 0) + .ThenBy(station => station.Id, StringComparer.Ordinal) + .FirstOrDefault(); + } + + private static Vector3 ResolveSupportPosition(ShipRuntime ship, StationRuntime station, ConstructionSiteRuntime? site, SimulationWorld world) + { + if (ship.DockedStationId is not null) + { + return GetShipDockedPosition(ship, station); + } + + if (site?.StationId is null && site is not null) + { + var anchorPosition = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? station.Position; + return GetResourceHoldPosition(anchorPosition, ship.Id, 78f); + } + + return GetConstructionHoldPosition(station, ship.Id); + } + + private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) => + ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f); + + private static void TrackHistory(ShipRuntime ship) + { + var plan = ship.ActivePlan; + var step = GetCurrentStep(plan); + var subTask = step is null || step.CurrentSubTaskIndex >= step.SubTasks.Count ? null : step.SubTasks[step.CurrentSubTaskIndex]; + var signature = $"{ship.State.ToContractValue()}|{plan?.Kind ?? "none"}|{step?.Kind ?? "none"}|{subTask?.Kind ?? "none"}|{GetShipCargoAmount(ship):0.0}"; + if (ship.LastSignature == signature) + { + return; + } + + ship.LastSignature = signature; + ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} plan={plan?.Kind ?? "none"} step={step?.Kind ?? "none"} subTask={subTask?.Kind ?? "none"} cargo={GetShipCargoAmount(ship):0.#}"); + if (ship.History.Count > 24) + { + ship.History.RemoveAt(0); + } + } + + private static void EmitStateEvents(ShipRuntime ship, ShipState previousState, string? previousPlanId, string? previousStepId, ICollection events) + { + var currentPlanId = ship.ActivePlan?.Id; + var currentStepId = GetCurrentStep(ship.ActivePlan)?.Id; + var occurredAtUtc = DateTimeOffset.UtcNow; + if (previousState != ship.State) + { + events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Name} state {previousState.ToContractValue()} -> {ship.State.ToContractValue()}.", occurredAtUtc)); + } + + if (!string.Equals(previousPlanId, currentPlanId, StringComparison.Ordinal)) + { + events.Add(new SimulationEventRecord("ship", ship.Id, "plan-changed", $"{ship.Definition.Name} switched active plan.", occurredAtUtc)); + } + + if (!string.Equals(previousStepId, currentStepId, StringComparison.Ordinal)) + { + events.Add(new SimulationEventRecord("ship", ship.Id, "step-changed", $"{ship.Definition.Name} advanced plan step.", occurredAtUtc)); + } + } + + private static void CompleteStationFoundation(SimulationWorld world, StationRuntime supportStation, ConstructionSiteRuntime site) + { + var anchor = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId); + if (anchor is null || site.BlueprintId is null) + { + site.State = ConstructionSiteStateKinds.Destroyed; + return; + } + + var station = new StationRuntime + { + Id = $"station-{world.Stations.Count + 1}", + SystemId = site.SystemId, + Label = BuildFoundedStationLabel(site.TargetDefinitionId), + Category = "station", + Objective = DetermineFoundationObjective(site.TargetDefinitionId), + Color = world.Factions.FirstOrDefault(candidate => candidate.Id == site.FactionId)?.Color ?? supportStation.Color, + Position = anchor.Position, + FactionId = site.FactionId, + CelestialId = site.CelestialId, + Health = 600f, + MaxHealth = 600f, + }; + + foreach (var moduleId in GetFoundationModules(world, site.BlueprintId)) + { + AddStationModule(world, station, moduleId); + } + + world.Stations.Add(station); + StationLifecycleService.EnsureStationCommander(world, station); + anchor.OccupyingStructureId = station.Id; + site.StationId = station.Id; + PrepareNextConstructionSiteStep(world, station, site); + } + + private static IReadOnlyList GetFoundationModules(SimulationWorld world, string primaryModuleId) + { + var modules = new List { "module_arg_dock_m_01_lowtech" }; + foreach (var itemId in world.ProductionGraph.OutputsByModuleId.GetValueOrDefault(primaryModuleId, [])) + { + if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) + { + var storageModule = GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind); + if (storageModule is not null && !modules.Contains(storageModule, StringComparer.Ordinal)) + { + modules.Add(storageModule); + } + } + } + + if (!modules.Contains("module_arg_stor_container_m_01", StringComparer.Ordinal)) + { + modules.Add("module_arg_stor_container_m_01"); + } + + if (!string.Equals(primaryModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)) + { + modules.Add("module_gen_prod_energycells_01"); + } + + modules.Add(primaryModuleId); + return modules.Distinct(StringComparer.Ordinal).ToList(); + } + + private static string DetermineFoundationObjective(string commodityId) => + commodityId switch + { + "energycells" => "power", + "water" => "water", + "refinedmetals" => "refinery", + "hullparts" => "hullparts", + "claytronics" => "claytronics", + "shipyard" => "shipyard", + _ => "general", + }; + + private static string BuildFoundedStationLabel(string commodityId) => + $"{char.ToUpperInvariant(commodityId[0])}{commodityId[1..]} Foundry"; +} diff --git a/apps/backend/Ships/AI/ShipAiService.Planning.Behaviors.cs b/apps/backend/Ships/AI/ShipAiService.Planning.Behaviors.cs new file mode 100644 index 0000000..00653b7 --- /dev/null +++ b/apps/backend/Ships/AI/ShipAiService.Planning.Behaviors.cs @@ -0,0 +1,319 @@ +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; + +namespace SpaceGame.Api.Ships.AI; + +public sealed partial class ShipAiService +{ + private ShipPlanRuntime BuildBehaviorFallbackPlan(SimulationWorld world, ShipRuntime ship) + { + var (behaviorKind, sourceId) = ResolveBehaviorSource(world, ship); + var failureReason = ship.LastAccessFailureReason; + if (string.Equals(behaviorKind, Idle, StringComparison.Ordinal)) + { + return CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "Idle"); + } + + if (IsBehaviorBlockingFailure(behaviorKind, failureReason)) + { + return CreateBlockedPlan( + ship, + AiPlanSourceKind.DefaultBehavior, + sourceId, + DescribeBehaviorFallbackSummary(world, ship, behaviorKind, failureReason), + failureReason!); + } + + return CreateIdlePlan( + ship, + AiPlanSourceKind.DefaultBehavior, + sourceId, + DescribeBehaviorFallbackSummary(world, ship, behaviorKind, failureReason)); + } + + private static bool IsBehaviorBlockingFailure(string behaviorKind, string? failureReason) => failureReason switch + { + "missing-item" => true, + "no-suitable-buyer" => true, + "no-mineable-node" when string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal) => true, + _ => false, + }; + + private static string DescribeBehaviorFallbackSummary(SimulationWorld world, ShipRuntime ship, string behaviorKind, string? failureReason) + { + var assignment = ResolveAssignment(world, ship); + var systemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; + var itemId = assignment?.ItemId ?? ship.DefaultBehavior.ItemId ?? "resource"; + + return failureReason switch + { + "missing-item" => "No mining ware configured", + "no-suitable-buyer" => $"No buyer for {itemId} in {systemId}", + "no-mineable-node" when string.Equals(behaviorKind, LocalAutoMine, StringComparison.Ordinal) => $"No {itemId} to mine in {systemId}", + "no-mineable-node" => "No mineable node", + "no-home-station" => "No home station", + "no-trade-route" => "No trade route", + "no-fleet-to-supply" => "No fleet to supply", + "station-missing" => "No station to dock", + "target-ship-missing" => "No ship to follow", + "target-missing" => "No object target", + "no-salvage-target" => "No salvage target", + "no-repeat-orders" => "No repeat orders", + "no-construction-site" => "No construction site", + "support-station-missing" => "No support station", + _ => "Idle", + }; + } + + private ShipPlanRuntime BuildTradePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, TradeRoutePlan route, string summary) + { + return CreatePlan( + ship, + sourceKind, + sourceId, + ShipOrderKinds.TradeRoute, + summary, + [ + CreateStep("step-acquire", "acquire-cargo", $"Load {route.ItemId} at {route.SourceStation.Label}", + [ + CreateSubTask("sub-acquire-travel", ShipTaskKinds.Travel, $"Travel to {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, MathF.Max(route.SourceStation.Radius + 12f, 12f), 0f), + CreateSubTask("sub-acquire-dock", ShipTaskKinds.Dock, $"Dock at {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, 4f, 0f), + CreateSubTask("sub-acquire-load", ShipTaskKinds.LoadCargo, $"Load {route.ItemId}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, 0f, ship.Definition.GetTotalCargoCapacity(), itemId: route.ItemId), + CreateSubTask("sub-acquire-undock", ShipTaskKinds.Undock, $"Undock from {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, MathF.Max(4f, 12f), 0f) + ]), + CreateStep("step-deliver", "deliver-cargo", $"Deliver {route.ItemId} to {route.DestinationStation.Label}", + [ + CreateSubTask("sub-route-travel", ShipTaskKinds.Travel, $"Travel to {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, MathF.Max(route.DestinationStation.Radius + 12f, 12f), 0f), + CreateSubTask("sub-route-dock", ShipTaskKinds.Dock, $"Dock at {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, 4f, 0f), + CreateSubTask("sub-route-unload", ShipTaskKinds.UnloadCargo, $"Unload {route.ItemId}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, 0f, ship.Definition.GetTotalCargoCapacity(), itemId: route.ItemId), + CreateSubTask("sub-route-undock", ShipTaskKinds.Undock, $"Undock from {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, MathF.Max(4f, 12f), 0f) + ]) + ]); + } + + private ShipPlanRuntime BuildFleetSupplyPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, FleetSupplyPlan plan) + { + return CreatePlan( + ship, + sourceKind, + sourceId, + SupplyFleet, + plan.Summary, + [ + CreateStep("step-fleet-acquire", "acquire-cargo", $"Load {plan.ItemId} at {plan.SourceStation.Label}", + [ + CreateSubTask("sub-fleet-source-travel", ShipTaskKinds.Travel, $"Travel to {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, MathF.Max(plan.SourceStation.Radius + 12f, 12f), 0f), + CreateSubTask("sub-fleet-source-dock", ShipTaskKinds.Dock, $"Dock at {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 4f, 0f), + CreateSubTask("sub-fleet-source-load", ShipTaskKinds.LoadCargo, $"Load {plan.ItemId}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 0f, plan.Amount, itemId: plan.ItemId), + CreateSubTask("sub-fleet-source-undock", ShipTaskKinds.Undock, $"Undock from {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 12f, 0f), + ]), + CreateStep("step-fleet-deliver", "deliver-fleet", $"Deliver {plan.ItemId} to {plan.TargetShip.Definition.Name}", + [ + CreateSubTask("sub-fleet-follow", ShipTaskKinds.FollowTarget, $"Rendezvous with {plan.TargetShip.Definition.Name}", plan.TargetShip.SystemId, plan.TargetShip.Position, plan.TargetShip.Id, plan.Radius, 6f), + CreateSubTask("sub-fleet-transfer", ShipTaskKinds.TransferCargoToShip, $"Transfer {plan.ItemId}", plan.TargetShip.SystemId, plan.TargetShip.Position, plan.TargetShip.Id, plan.Radius, plan.Amount, itemId: plan.ItemId), + ]) + ]); + } + + private ShipPlanRuntime BuildConstructionPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ConstructionSiteRuntime site, StationRuntime supportStation, string summary) + { + var targetPosition = site.StationId is null ? supportStation.Position : supportStation.Position; + return CreatePlan( + ship, + sourceKind, + sourceId, + "construction-support", + summary, + [ + CreateStep("step-construction-deliver", "deliver-materials", $"Deliver materials to {site.Id}", + [ + CreateSubTask("sub-construction-travel", ShipTaskKinds.Travel, $"Travel to support station {supportStation.Label}", supportStation.SystemId, targetPosition, supportStation.Id, MathF.Max(supportStation.Radius + 18f, 18f), 0f), + CreateSubTask("sub-construction-deliver", ShipTaskKinds.DeliverConstruction, $"Deliver materials to {site.Id}", site.SystemId, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f) + ]), + CreateStep("step-construction-build", "build-site", $"Build {site.Id}", + [ + CreateSubTask("sub-construction-build", ShipTaskKinds.BuildConstructionSite, $"Build {site.Id}", site.SystemId, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f) + ]) + ]); + } + + private ShipPlanRuntime BuildAttackPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string? targetSystemId, string summary) + { + return CreatePlan( + ship, + sourceKind, + sourceId, + ShipOrderKinds.AttackTarget, + summary, + [ + CreateStep("step-attack", ShipOrderKinds.AttackTarget, summary, + [ + CreateSubTask("sub-attack", ShipTaskKinds.AttackTarget, summary, targetSystemId ?? ship.SystemId, ship.Position, targetEntityId, 26f, 0f) + ]) + ]); + } + + private ShipPlanRuntime BuildDockAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, StationRuntime station, float waitSeconds, string summary) + { + return CreatePlan( + ship, + sourceKind, + sourceId, + ShipOrderKinds.DockAndWait, + summary, + [ + CreateStep("step-dock-wait-travel", "travel", $"Travel to {station.Label}", + [ + CreateSubTask("sub-dock-wait-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(station.Radius + 12f, 12f), 0f), + CreateSubTask("sub-dock-wait-dock", ShipTaskKinds.Dock, $"Dock at {station.Label}", station.SystemId, station.Position, station.Id, 4f, 0f), + CreateSubTask("sub-dock-wait-hold", ShipTaskKinds.HoldPosition, $"Wait at {station.Label}", station.SystemId, station.Position, station.Id, 0f, waitSeconds), + ]) + ]); + } + + private ShipPlanRuntime BuildFlyAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, float waitSeconds, string summary) + { + return CreatePlan( + ship, + sourceKind, + sourceId, + ShipOrderKinds.FlyAndWait, + summary, + [ + CreateStep("step-fly-wait", ShipOrderKinds.FlyAndWait, summary, + [ + CreateSubTask("sub-fly-wait-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, null, 6f, 0f), + CreateSubTask("sub-fly-wait-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, null, 0f, waitSeconds), + ]) + ]); + } + + private ShipPlanRuntime BuildFlyToObjectPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, string targetEntityId, string summary) + { + return CreatePlan( + ship, + sourceKind, + sourceId, + ShipOrderKinds.FlyToObject, + summary, + [ + CreateStep("step-fly-object", ShipOrderKinds.FlyToObject, summary, + [ + CreateSubTask("sub-fly-object-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, targetEntityId, 8f, 0f), + CreateSubTask("sub-fly-object-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, targetEntityId, 0f, MathF.Max(1f, ship.DefaultBehavior.WaitSeconds)), + ]) + ]); + } + + private ShipPlanRuntime BuildFollowShipPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ShipRuntime targetShip, float radius, float durationSeconds, string summary) + { + return BuildFollowPlan(ship, sourceKind, sourceId, targetShip.Id, targetShip.SystemId, targetShip.Position, radius, durationSeconds, summary); + } + + private ShipPlanRuntime BuildFollowPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string targetSystemId, Vector3 targetPosition, float radius, float durationSeconds, string summary) + { + return CreatePlan( + ship, + sourceKind, + sourceId, + ShipOrderKinds.FollowShip, + summary, + [ + CreateStep("step-follow", "follow-target", summary, + [ + CreateSubTask("sub-follow", ShipTaskKinds.FollowTarget, summary, targetSystemId, targetPosition, targetEntityId, radius, durationSeconds), + ]) + ]); + } + + private ShipPlanRuntime CreateIdlePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary) + { + return CreatePlan( + ship, + sourceKind, + sourceId, + Idle, + summary, + [ + CreateStep("step-idle", ShipOrderKinds.HoldPosition, summary, + [ + CreateSubTask("sub-idle", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 1.5f) + ]) + ]); + } + + private ShipPlanRuntime CreateBlockedPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary, string blockingReason) + { + var subTask = CreateSubTask("sub-blocked", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 0f); + subTask.Status = WorkStatus.Blocked; + subTask.BlockingReason = blockingReason; + + var step = CreateStep("step-blocked", "blocked", summary, [subTask]); + step.Status = AiPlanStepStatus.Blocked; + step.BlockingReason = blockingReason; + + var plan = CreatePlan(ship, sourceKind, sourceId, "blocked", summary, [step]); + plan.Status = AiPlanStatus.Blocked; + plan.FailureReason = blockingReason; + return plan; + } + + private static ShipPlanRuntime CreatePlan( + ShipRuntime ship, + AiPlanSourceKind sourceKind, + string sourceId, + string kind, + string summary, + IReadOnlyList steps) + { + var plan = new ShipPlanRuntime + { + Id = $"plan-{ship.Id}-{Guid.NewGuid():N}", + SourceKind = sourceKind, + SourceId = sourceId, + Kind = kind, + Summary = summary, + }; + plan.Steps.AddRange(steps); + return plan; + } + + private static ShipPlanStepRuntime CreateStep(string id, string kind, string summary, IReadOnlyList subTasks) + { + var step = new ShipPlanStepRuntime + { + Id = id, + Kind = kind, + Summary = summary, + }; + step.SubTasks.AddRange(subTasks); + return step; + } + + private static ShipSubTaskRuntime CreateSubTask( + string id, + string kind, + string summary, + string targetSystemId, + Vector3 targetPosition, + string? targetEntityId, + float threshold, + float amount, + string? itemId = null, + string? moduleId = null, + string? targetNodeId = null) => + new() + { + Id = id, + Kind = kind, + Summary = summary, + TargetSystemId = targetSystemId, + TargetPosition = targetPosition, + TargetEntityId = targetEntityId, + TargetNodeId = targetNodeId, + ItemId = itemId, + ModuleId = moduleId, + Threshold = threshold, + Amount = amount, + }; +} diff --git a/apps/backend/Ships/AI/ShipAiService.Planning.Orders.cs b/apps/backend/Ships/AI/ShipAiService.Planning.Orders.cs new file mode 100644 index 0000000..c5fb2fe --- /dev/null +++ b/apps/backend/Ships/AI/ShipAiService.Planning.Orders.cs @@ -0,0 +1,461 @@ +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; +using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; +using static SpaceGame.Api.Stations.Simulation.StationSimulationService; + +namespace SpaceGame.Api.Ships.AI; + +public sealed partial class ShipAiService +{ + private ShipPlanRuntime? BuildEmergencyPlan(SimulationWorld world, ShipRuntime ship) + { + var policy = ResolvePolicy(world, ship.PolicySetId); + if (policy is null) + { + return null; + } + + var hullRatio = ship.Definition.Hull <= 0.01f ? 1f : ship.Health / ship.Definition.Hull; + if (hullRatio > policy.FleeHullRatio) + { + return null; + } + + var hostileNearby = world.Ships.Any(candidate => + candidate.Health > 0f && + candidate.FactionId != ship.FactionId && + candidate.SystemId == ship.SystemId && + candidate.Position.DistanceTo(ship.Position) <= 200f); + if (!hostileNearby) + { + return null; + } + + var safeStation = world.Stations + .Where(station => station.FactionId == ship.FactionId) + .OrderByDescending(station => station.SystemId == ship.SystemId ? 1 : 0) + .ThenBy(station => station.Position.DistanceTo(ship.Position)) + .FirstOrDefault(); + + var plan = new ShipPlanRuntime + { + Id = $"plan-{ship.Id}-safety-{Guid.NewGuid():N}", + SourceKind = AiPlanSourceKind.Rule, + SourceId = ShipOrderKinds.Flee, + Kind = "safety-flee", + Summary = "Emergency retreat", + }; + + if (safeStation is null) + { + plan.Steps.Add(CreateStep("step-flee-hold", ShipOrderKinds.HoldPosition, "Hold position away from hostiles", + [ + CreateSubTask("sub-flee-hold", ShipTaskKinds.HoldPosition, "Hold safe position", ship.SystemId, ship.Position, null, 0f, 3f) + ])); + return plan; + } + + plan.Steps.Add(CreateStep("step-flee-travel", "travel", "Travel to safe station", + [ + CreateSubTask("sub-flee-travel", ShipTaskKinds.Travel, $"Travel to {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, MathF.Max(balance.ArrivalThreshold, safeStation.Radius + 12f), 0f) + ])); + plan.Steps.Add(CreateStep("step-flee-dock", "dock", "Dock at safe station", + [ + CreateSubTask("sub-flee-dock", ShipTaskKinds.Dock, $"Dock at {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, 4f, 0f) + ])); + return plan; + } + + private ShipPlanRuntime? BuildOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + return order.Kind switch + { + var kind when string.Equals(kind, ShipOrderKinds.Move, StringComparison.Ordinal) => BuildMovePlan(ship, order), + var kind when string.Equals(kind, ShipOrderKinds.DockAtStation, StringComparison.Ordinal) => BuildDockOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.DockAndWait, StringComparison.Ordinal) => BuildDockAndWaitOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.FlyAndWait, StringComparison.Ordinal) => BuildFlyAndWaitOrderPlan(ship, order), + var kind when string.Equals(kind, ShipOrderKinds.FlyToObject, StringComparison.Ordinal) => BuildFlyToObjectOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.FollowShip, StringComparison.Ordinal) => BuildFollowShipOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.TradeRoute, StringComparison.Ordinal) => BuildTradeOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.MineAndDeliver, StringComparison.Ordinal) => BuildMineOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.MineLocal, StringComparison.Ordinal) => BuildMineLocalOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.MineAndDeliverRun, StringComparison.Ordinal) => BuildMineAndDeliverRunOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.SellMinedCargo, StringComparison.Ordinal) => BuildSellMinedCargoOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.SupplyFleetRun, StringComparison.Ordinal) => BuildSupplyFleetOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.SalvageRun, StringComparison.Ordinal) => BuildAutoSalvageOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.BuildAtSite, StringComparison.Ordinal) => BuildBuildOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.AttackTarget, StringComparison.Ordinal) => BuildAttackOrderPlan(world, ship, order), + var kind when string.Equals(kind, ShipOrderKinds.HoldPosition, StringComparison.Ordinal) => BuildHoldOrderPlan(ship, order), + _ => null, + }; + } + + private static (string BehaviorKind, string SourceId) ResolveBehaviorSource(SimulationWorld world, ShipRuntime ship) + { + var assignment = ResolveAssignment(world, ship); + return assignment is null + ? (ship.DefaultBehavior.Kind, ship.DefaultBehavior.Kind) + : (assignment.BehaviorKind, assignment.ObjectiveId); + } + + private ShipPlanRuntime BuildMovePlan(ShipRuntime ship, ShipOrderRuntime order) + { + var targetSystemId = order.TargetSystemId ?? ship.SystemId; + var targetPosition = order.TargetPosition ?? ship.Position; + return CreatePlan( + ship, + AiPlanSourceKind.Order, + order.Id, + ShipOrderKinds.Move, + order.Label ?? "Move order", + [ + CreateStep("step-move", "travel", order.Label ?? "Travel", + [ + CreateSubTask("sub-move-travel", ShipTaskKinds.Travel, order.Label ?? "Travel", targetSystemId, targetPosition, order.TargetEntityId, 0f, 0f) + ]) + ]); + } + + private ShipPlanRuntime? BuildDockOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var station = ResolveStation(world, order.TargetEntityId ?? order.DestinationStationId); + if (station is null) + { + order.FailureReason = "station-missing"; + return null; + } + + return CreatePlan( + ship, + AiPlanSourceKind.Order, + order.Id, + "dock-at-station", + order.Label ?? $"Dock at {station.Label}", + [ + CreateStep("step-dock-travel", "travel", $"Travel to {station.Label}", + [ + CreateSubTask("sub-dock-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(balance.ArrivalThreshold, station.Radius + 12f), 0f) + ]), + CreateStep("step-dock", "dock", $"Dock at {station.Label}", + [ + CreateSubTask("sub-dock", ShipTaskKinds.Dock, $"Dock at {station.Label}", station.SystemId, station.Position, station.Id, 4f, 0f) + ]) + ]); + } + + private ShipPlanRuntime? BuildTradeOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + if (order.SourceStationId is null || order.DestinationStationId is null || order.ItemId is null) + { + order.FailureReason = "trade-order-incomplete"; + return null; + } + + var route = ResolveTradeRoute(world, order.ItemId, order.SourceStationId, order.DestinationStationId); + if (route is null) + { + order.FailureReason = "trade-route-missing"; + return null; + } + + return BuildTradePlan(ship, AiPlanSourceKind.Order, order.Id, route, order.Label ?? route.Summary); + } + + private ShipPlanRuntime? BuildMineOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var systemId = order.TargetSystemId ?? ship.SystemId; + var itemId = order.ItemId; + if (string.IsNullOrWhiteSpace(itemId)) + { + order.FailureReason = "mine-order-item-missing"; + return null; + } + + var node = ResolveNode(world, order.NodeId); + if (node is not null) + { + if (!string.Equals(node.SystemId, systemId, StringComparison.Ordinal)) + { + order.FailureReason = "mine-order-node-system-mismatch"; + return null; + } + + if (!string.Equals(node.ItemId, itemId, StringComparison.Ordinal)) + { + order.FailureReason = "mine-order-node-item-mismatch"; + return null; + } + } + else + { + node = SelectLocalMiningNode(world, ship, systemId, itemId); + } + + if (node is null) + { + order.FailureReason = "mine-order-node-missing"; + return null; + } + + return BuildLocalMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {itemId} in {systemId}"); + } + + private ShipPlanRuntime? BuildMineLocalOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var node = ResolveNode(world, order.NodeId); + if (node is null) + { + order.FailureReason = "mine-order-incomplete"; + return null; + } + + return BuildLocalMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, order.Label ?? $"Mine {node.ItemId}"); + } + + private ShipPlanRuntime? BuildMineAndDeliverRunOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var node = ResolveNode(world, order.NodeId); + var buyer = ResolveStation(world, order.DestinationStationId); + if (node is null || buyer is null) + { + order.FailureReason = "mine-and-deliver-order-incomplete"; + return null; + } + + return BuildMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, buyer, order.Label ?? $"Mine {node.ItemId} for {buyer.Label}"); + } + + private ShipPlanRuntime? BuildSellMinedCargoOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var buyer = ResolveStation(world, order.DestinationStationId ?? order.TargetEntityId); + if (buyer is null || string.IsNullOrWhiteSpace(order.ItemId)) + { + order.FailureReason = "sell-order-incomplete"; + return null; + } + + return BuildLocalMiningDeliveryPlan(ship, AiPlanSourceKind.Order, order.Id, buyer, order.ItemId, order.Label ?? $"Sell {order.ItemId}"); + } + + private ShipPlanRuntime? BuildAutoSalvageOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var homeStation = ResolveStation(world, order.SourceStationId ?? ship.DefaultBehavior.HomeStationId); + var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == order.TargetEntityId && candidate.RemainingAmount > 0.01f); + if (homeStation is null || wreck is null) + { + order.FailureReason = "salvage-order-incomplete"; + return null; + } + + var approach = GetFormationPosition(wreck.Position, ship.Id, MathF.Max(8f, order.Radius > 0f ? order.Radius : ship.DefaultBehavior.Radius * 0.25f)); + return CreatePlan( + ship, + AiPlanSourceKind.Order, + order.Id, + AutoSalvage, + order.Label ?? $"Salvage {wreck.ItemId}", + [ + CreateStep("step-salvage-collect", "salvage", $"Salvage {wreck.ItemId}", + [ + CreateSubTask("sub-salvage-travel", ShipTaskKinds.Travel, $"Travel to wreck {wreck.Id}", wreck.SystemId, approach, wreck.Id, 8f, 0f), + CreateSubTask("sub-salvage-work", ShipTaskKinds.SalvageWreck, $"Salvage {wreck.ItemId}", wreck.SystemId, approach, wreck.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: wreck.ItemId), + ]), + CreateStep("step-salvage-deliver", "deliver-salvage", $"Deliver salvage to {homeStation.Label}", + [ + CreateSubTask("sub-salvage-deliver-travel", ShipTaskKinds.Travel, $"Travel to {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, MathF.Max(homeStation.Radius + 12f, 12f), 0f), + CreateSubTask("sub-salvage-deliver-dock", ShipTaskKinds.Dock, $"Dock at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 4f, 0f), + CreateSubTask("sub-salvage-deliver-unload", ShipTaskKinds.UnloadCargo, $"Unload salvage at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 0f, ship.Definition.GetTotalCargoCapacity(), itemId: wreck.ItemId), + CreateSubTask("sub-salvage-deliver-undock", ShipTaskKinds.Undock, $"Undock from {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 12f, 0f), + ]) + ]); + } + + private ShipPlanRuntime? BuildSupplyFleetOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var sourceStation = ResolveStation(world, order.SourceStationId); + var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == order.TargetEntityId && candidate.Health > 0f); + if (sourceStation is null || targetShip is null || string.IsNullOrWhiteSpace(order.ItemId)) + { + order.FailureReason = "supply-fleet-order-incomplete"; + return null; + } + + var amount = MathF.Min( + MathF.Max(10f, ship.Definition.GetTotalCargoCapacity() * 0.5f), + GetInventoryAmount(sourceStation.Inventory, order.ItemId)); + if (amount <= 0.01f) + { + order.FailureReason = "supply-item-unavailable"; + return null; + } + + var plan = new FleetSupplyPlan( + sourceStation, + targetShip, + order.ItemId, + amount, + MathF.Max(16f, order.Radius), + order.Label ?? $"Supply {targetShip.Definition.Name} with {order.ItemId}"); + return BuildFleetSupplyPlan(ship, AiPlanSourceKind.Order, order.Id, plan); + } + + private ShipPlanRuntime? BuildBuildOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == (order.ConstructionSiteId ?? order.TargetEntityId)); + if (site is null) + { + order.FailureReason = "construction-site-missing"; + return null; + } + + var supportStation = ResolveSupportStation(world, ship, site); + if (supportStation is null) + { + order.FailureReason = "support-station-missing"; + return null; + } + + return BuildConstructionPlan(ship, AiPlanSourceKind.Order, order.Id, site, supportStation, order.Label ?? $"Build {site.BlueprintId}"); + } + + private ShipPlanRuntime? BuildAttackOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var targetId = order.TargetEntityId; + if (targetId is null) + { + order.FailureReason = "attack-target-missing"; + return null; + } + + return BuildAttackPlan(ship, AiPlanSourceKind.Order, order.Id, targetId, order.TargetSystemId, order.Label ?? "Attack target"); + } + + private ShipPlanRuntime BuildHoldOrderPlan(ShipRuntime ship, ShipOrderRuntime order) + { + return CreatePlan( + ship, + AiPlanSourceKind.Order, + order.Id, + ShipOrderKinds.HoldPosition, + order.Label ?? "Hold position", + [ + CreateStep("step-hold", ShipOrderKinds.HoldPosition, order.Label ?? "Hold position", + [ + CreateSubTask("sub-hold", ShipTaskKinds.HoldPosition, order.Label ?? "Hold position", order.TargetSystemId ?? ship.SystemId, order.TargetPosition ?? ship.Position, order.TargetEntityId, 0f, 3f) + ]) + ]); + } + + private ShipPlanRuntime? BuildDockAndWaitOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var station = ResolveStation(world, order.TargetEntityId ?? order.DestinationStationId ?? ship.DefaultBehavior.HomeStationId); + if (station is null) + { + order.FailureReason = "station-missing"; + return null; + } + + return BuildDockAndWaitPlan(ship, AiPlanSourceKind.Order, order.Id, station, MathF.Max(1f, order.WaitSeconds), order.Label ?? $"Dock and wait at {station.Label}"); + } + + private ShipPlanRuntime BuildFlyAndWaitOrderPlan(ShipRuntime ship, ShipOrderRuntime order) + { + var systemId = order.TargetSystemId ?? ship.SystemId; + var targetPosition = order.TargetPosition ?? ship.Position; + return BuildFlyAndWaitPlan(ship, AiPlanSourceKind.Order, order.Id, systemId, targetPosition, MathF.Max(1f, order.WaitSeconds), order.Label ?? "Fly and wait"); + } + + private ShipPlanRuntime? BuildFlyToObjectOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var targetEntityId = order.TargetEntityId; + if (targetEntityId is null) + { + order.FailureReason = "target-missing"; + return null; + } + + var objectTarget = ResolveObjectTarget(world, targetEntityId); + if (objectTarget is null) + { + order.FailureReason = "target-missing"; + return null; + } + + return BuildFlyToObjectPlan(ship, AiPlanSourceKind.Order, order.Id, objectTarget.Value.SystemId, objectTarget.Value.Position, targetEntityId, order.Label ?? $"Fly to {targetEntityId}"); + } + + private ShipPlanRuntime? BuildFollowShipOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) + { + var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == order.TargetEntityId && candidate.Health > 0f); + if (targetShip is null) + { + order.FailureReason = "target-ship-missing"; + return null; + } + + return BuildFollowShipPlan(ship, AiPlanSourceKind.Order, order.Id, targetShip, MathF.Max(order.Radius, 18f), MathF.Max(2f, order.WaitSeconds), order.Label ?? $"Follow {targetShip.Definition.Name}"); + } + + private ShipPlanRuntime BuildMiningPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, StationRuntime homeStation, string summary) + { + var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); + return CreatePlan( + ship, + sourceKind, + sourceId, + ShipOrderKinds.MineAndDeliver, + summary, + [ + CreateStep("step-mine", "mine", $"Mine {node.ItemId}", + [ + CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} node", node.SystemId, extractionPosition, node.Id, 8f, 0f), + CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity()) + ]), + CreateStep("step-deliver", "deliver", $"Deliver {node.ItemId} to {homeStation.Label}", + [ + CreateSubTask("sub-deliver-travel", ShipTaskKinds.Travel, $"Travel to {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, MathF.Max(homeStation.Radius + 12f, 12f), 0f), + CreateSubTask("sub-deliver-dock", ShipTaskKinds.Dock, $"Dock at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 4f, 0f), + CreateSubTask("sub-deliver-unload", ShipTaskKinds.UnloadCargo, $"Unload at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 0f, ship.Definition.GetTotalCargoCapacity()), + CreateSubTask("sub-deliver-undock", ShipTaskKinds.Undock, $"Undock from {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, MathF.Max(4f, 12f), 0f) + ]) + ]); + } + + private ShipPlanRuntime BuildLocalMiningPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, string summary) + { + var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); + return CreatePlan( + ship, + sourceKind, + sourceId, + ShipOrderKinds.MineLocal, + summary, + [ + CreateStep("step-mine", "mine", $"Mine {node.ItemId}", + [ + CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} node", node.SystemId, extractionPosition, node.Id, 8f, 0f), + CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.GetTotalCargoCapacity(), itemId: node.ItemId) + ]) + ]); + } + + private ShipPlanRuntime BuildLocalMiningDeliveryPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, StationRuntime buyer, string itemId, string summary) + { + var amount = MathF.Max(1f, GetInventoryAmount(ship.Inventory, itemId)); + return CreatePlan( + ship, + sourceKind, + sourceId, + ShipOrderKinds.SellMinedCargo, + summary, + [ + CreateStep("step-deliver", "deliver", $"Deliver {itemId} to {buyer.Label}", + [ + CreateSubTask("sub-deliver-travel", ShipTaskKinds.Travel, $"Travel to {buyer.Label}", buyer.SystemId, buyer.Position, buyer.Id, MathF.Max(buyer.Radius + 12f, 12f), 0f), + CreateSubTask("sub-deliver-dock", ShipTaskKinds.Dock, $"Dock at {buyer.Label}", buyer.SystemId, buyer.Position, buyer.Id, 4f, 0f), + CreateSubTask("sub-deliver-unload", ShipTaskKinds.UnloadCargo, $"Unload {itemId} at {buyer.Label}", buyer.SystemId, buyer.Position, buyer.Id, 0f, amount, itemId: itemId), + CreateSubTask("sub-deliver-undock", ShipTaskKinds.Undock, $"Undock from {buyer.Label}", buyer.SystemId, buyer.Position, buyer.Id, MathF.Max(4f, 12f), 0f) + ]) + ]); + } +} diff --git a/apps/backend/Ships/AI/ShipAiService.cs b/apps/backend/Ships/AI/ShipAiService.cs new file mode 100644 index 0000000..4df4d3e --- /dev/null +++ b/apps/backend/Ships/AI/ShipAiService.cs @@ -0,0 +1,216 @@ +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; + +namespace SpaceGame.Api.Ships.AI; + +public sealed partial class ShipAiService +{ + private const float WarpEngageDistanceKilometers = 250_000f; + private const float FrigateDps = 7f; + private const float DestroyerDps = 12f; + private const float CruiserDps = 18f; + private const float CapitalDps = 26f; + + private readonly IBalanceService balance; + + public ShipAiService(IBalanceService balance) + { + this.balance = balance; + } + + internal void UpdateShip(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection events) + { + if (ship.ReplanCooldownSeconds > 0f) + { + ship.ReplanCooldownSeconds = MathF.Max(0f, ship.ReplanCooldownSeconds - deltaSeconds); + } + + var previousState = ship.State; + var previousPlanId = ship.ActivePlan?.Id; + var previousStepId = GetCurrentStep(ship.ActivePlan)?.Id; + + EnsurePlan(world, ship, events); + ExecutePlan(world, ship, deltaSeconds, events); + TrackHistory(ship); + EmitStateEvents(ship, previousState, previousPlanId, previousStepId, events); + } + + private void EnsurePlan(SimulationWorld world, ShipRuntime ship, ICollection events) + { + var emergencyPlan = BuildEmergencyPlan(world, ship); + if (emergencyPlan is not null) + { + ship.LastReplanReason = "rule-safety"; + ReplacePlan(ship, emergencyPlan, "rule-safety", events); + return; + } + + SyncBehaviorOrders(world, ship); + var topOrder = GetTopOrder(ship); + if (topOrder is not null && topOrder.Status == OrderStatus.Queued) + { + topOrder.Status = OrderStatus.Active; + } + + var desiredSourceKind = topOrder is null ? AiPlanSourceKind.DefaultBehavior : AiPlanSourceKind.Order; + var desiredSourceId = topOrder?.Id ?? ResolveBehaviorSource(world, ship).SourceId; + var currentPlan = ship.ActivePlan; + + if (currentPlan is not null + && currentPlan.Status is not AiPlanStatus.Completed and not AiPlanStatus.Failed and not AiPlanStatus.Interrupted + && currentPlan.SourceKind == desiredSourceKind + && string.Equals(currentPlan.SourceId, desiredSourceId, StringComparison.Ordinal) + && !ship.NeedsReplan) + { + return; + } + + if (ship.ReplanCooldownSeconds > 0f && currentPlan is null) + { + return; + } + + ShipPlanRuntime? nextPlan = desiredSourceKind == AiPlanSourceKind.Order + ? BuildOrderPlan(world, ship, topOrder!) + : BuildBehaviorFallbackPlan(world, ship); + + if (nextPlan is null) + { + nextPlan = CreateIdlePlan(ship, desiredSourceKind, desiredSourceId, "No viable plan"); + } + + if (nextPlan.Kind != Idle) + { + ship.LastAccessFailureReason = null; + } + + ReplacePlan(ship, nextPlan, "replanned", events); + } + + private void ExecutePlan(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection events) + { + var plan = ship.ActivePlan; + if (plan is null) + { + ship.State = ShipState.Idle; + ship.TargetPosition = ship.Position; + return; + } + + if (plan.CurrentStepIndex >= plan.Steps.Count) + { + CompletePlan(ship, plan, events); + return; + } + + plan.UpdatedAtUtc = DateTimeOffset.UtcNow; + + var step = plan.Steps[plan.CurrentStepIndex]; + if (step.Status == AiPlanStepStatus.Planned) + { + step.Status = AiPlanStepStatus.Running; + } + + if (step.CurrentSubTaskIndex >= step.SubTasks.Count) + { + CompleteStep(plan, step); + return; + } + + var subTask = step.SubTasks[step.CurrentSubTaskIndex]; + if (subTask.Status == WorkStatus.Pending) + { + subTask.Status = WorkStatus.Active; + } + else if (subTask.Status == WorkStatus.Blocked) + { + step.Status = AiPlanStepStatus.Blocked; + step.BlockingReason = subTask.BlockingReason; + plan.Status = AiPlanStatus.Blocked; + ship.State = ShipState.Blocked; + ship.TargetPosition = subTask.TargetPosition ?? ship.Position; + return; + } + + plan.Status = AiPlanStatus.Running; + + var outcome = UpdateSubTask(world, ship, step, subTask, deltaSeconds); + switch (outcome) + { + case SubTaskOutcome.Active: + step.Status = subTask.Status == WorkStatus.Blocked ? AiPlanStepStatus.Blocked : AiPlanStepStatus.Running; + plan.Status = subTask.Status == WorkStatus.Blocked ? AiPlanStatus.Blocked : AiPlanStatus.Running; + return; + case SubTaskOutcome.Completed: + subTask.Status = WorkStatus.Completed; + subTask.Progress = 1f; + step.CurrentSubTaskIndex += 1; + step.BlockingReason = null; + if (step.CurrentSubTaskIndex >= step.SubTasks.Count) + { + CompleteStep(plan, step); + } + + return; + case SubTaskOutcome.Failed: + subTask.Status = WorkStatus.Failed; + step.Status = AiPlanStepStatus.Failed; + plan.Status = AiPlanStatus.Failed; + plan.FailureReason = subTask.BlockingReason ?? "subtask-failed"; + ship.NeedsReplan = true; + ship.ReplanCooldownSeconds = 0.5f; + ship.LastReplanReason = plan.FailureReason; + return; + } + } + + private static void CompleteStep(ShipPlanRuntime plan, ShipPlanStepRuntime step) + { + step.Status = AiPlanStepStatus.Completed; + step.BlockingReason = null; + plan.CurrentStepIndex += 1; + if (plan.CurrentStepIndex >= plan.Steps.Count) + { + plan.Status = AiPlanStatus.Completed; + } + } + + private static void CompletePlan(ShipRuntime ship, ShipPlanRuntime plan, ICollection events) + { + plan.Status = AiPlanStatus.Completed; + var completedOrder = plan.SourceKind == AiPlanSourceKind.Order + ? ship.OrderQueue.FirstOrDefault(order => order.Id == plan.SourceId) + : null; + if (completedOrder is not null) + { + completedOrder.Status = OrderStatus.Completed; + ship.OrderQueue.RemoveAll(order => order.Id == completedOrder.Id); + if (completedOrder.SourceKind == ShipOrderSourceKind.Behavior + && string.Equals(completedOrder.SourceId, RepeatOrders, StringComparison.Ordinal) + && ship.DefaultBehavior.RepeatOrders.Count > 0) + { + ship.DefaultBehavior.RepeatIndex = (ship.DefaultBehavior.RepeatIndex + 1) % ship.DefaultBehavior.RepeatOrders.Count; + } + } + ship.ActivePlan = null; + ship.NeedsReplan = true; + ship.ReplanCooldownSeconds = 0.25f; + ship.LastReplanReason = "plan-completed"; + events.Add(new SimulationEventRecord("ship", ship.Id, "plan-completed", $"{ship.Definition.Name} completed {plan.Kind}.", DateTimeOffset.UtcNow)); + } + + private void ReplacePlan(ShipRuntime ship, ShipPlanRuntime nextPlan, string reason, ICollection events) + { + if (ship.ActivePlan is not null && ship.ActivePlan.Status is not AiPlanStatus.Completed and not AiPlanStatus.Failed) + { + ship.ActivePlan.Status = AiPlanStatus.Interrupted; + ship.ActivePlan.InterruptReason = reason; + } + + ship.ActivePlan = nextPlan; + ship.NeedsReplan = false; + ship.ReplanCooldownSeconds = 0f; + ship.LastReplanReason = reason; + events.Add(new SimulationEventRecord("ship", ship.Id, "plan-updated", $"{ship.Definition.Name} planned {nextPlan.Kind}.", DateTimeOffset.UtcNow)); + } +} diff --git a/apps/backend/Ships/AI/ShipBootstrapPolicy.cs b/apps/backend/Ships/AI/ShipBootstrapPolicy.cs new file mode 100644 index 0000000..3c528e7 --- /dev/null +++ b/apps/backend/Ships/AI/ShipBootstrapPolicy.cs @@ -0,0 +1,31 @@ +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; + +namespace SpaceGame.Api.Ships.AI; + +internal static class ShipBootstrapPolicy +{ + internal static ShipSkillProfileRuntime CreateSkills(ShipDefinition definition) + { + if (IsTransportShip(definition)) + { + return new ShipSkillProfileRuntime { Navigation = 3, Trade = 4, Mining = 1, Combat = 1, Construction = 1 }; + } + + if (IsConstructionShip(definition)) + { + return new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 1, Combat = 1, Construction = 4 }; + } + + if (IsMilitaryShip(definition)) + { + return new ShipSkillProfileRuntime { Navigation = 4, Trade = 1, Mining = 1, Combat = 4, Construction = 1 }; + } + + if (IsMiningShip(definition)) + { + return new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 4, Combat = 1, Construction = 1 }; + } + + return new ShipSkillProfileRuntime { Navigation = 3, Trade = 2, Mining = 1, Combat = 1, Construction = 1 }; + } +} diff --git a/apps/backend/Ships/Api/EnqueueShipOrderHandler.cs b/apps/backend/Ships/Api/EnqueueShipOrderHandler.cs index 9a4fac3..ddb3144 100644 --- a/apps/backend/Ships/Api/EnqueueShipOrderHandler.cs +++ b/apps/backend/Ships/Api/EnqueueShipOrderHandler.cs @@ -7,7 +7,6 @@ public sealed class EnqueueShipOrderHandler(WorldService worldService) : Endpoin public override void Configure() { Post("/api/ships/{shipId}/orders"); - AllowAnonymous(); } public override async Task HandleAsync(ShipOrderCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/Ships/Api/GetShipAutomationCatalogHandler.cs b/apps/backend/Ships/Api/GetShipAutomationCatalogHandler.cs new file mode 100644 index 0000000..a3aaf1c --- /dev/null +++ b/apps/backend/Ships/Api/GetShipAutomationCatalogHandler.cs @@ -0,0 +1,35 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Ships.Api; + +public sealed class GetShipAutomationCatalogHandler : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/api/ships/catalog"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken cancellationToken) + { + var snapshot = new ShipAutomationCatalogSnapshot( + ShipAutomationCatalog.Behaviors + .Select(definition => new ShipBehaviorDefinitionSnapshot( + definition.Id, + definition.Label, + definition.Category, + definition.SupportStatus.ToString(), + definition.Notes)) + .ToList(), + ShipAutomationCatalog.Orders + .Select(definition => new ShipOrderDefinitionSnapshot( + definition.Id, + definition.Label, + definition.Category, + definition.SupportStatus.ToString(), + definition.Notes)) + .ToList()); + + await SendOkAsync(snapshot, cancellationToken); + } +} diff --git a/apps/backend/Ships/Api/RemoveShipOrderHandler.cs b/apps/backend/Ships/Api/RemoveShipOrderHandler.cs index bc3b77d..bb6d750 100644 --- a/apps/backend/Ships/Api/RemoveShipOrderHandler.cs +++ b/apps/backend/Ships/Api/RemoveShipOrderHandler.cs @@ -13,7 +13,6 @@ public sealed class RemoveShipOrderHandler(WorldService worldService) : Endpoint public override void Configure() { Delete("/api/ships/{shipId}/orders/{orderId}"); - AllowAnonymous(); } public override async Task HandleAsync(RemoveShipOrderRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/Ships/Api/UpdateShipDefaultBehaviorHandler.cs b/apps/backend/Ships/Api/UpdateShipDefaultBehaviorHandler.cs index d77fe72..9a57be6 100644 --- a/apps/backend/Ships/Api/UpdateShipDefaultBehaviorHandler.cs +++ b/apps/backend/Ships/Api/UpdateShipDefaultBehaviorHandler.cs @@ -7,7 +7,6 @@ public sealed class UpdateShipDefaultBehaviorHandler(WorldService worldService) public override void Configure() { Put("/api/ships/{shipId}/default-behavior"); - AllowAnonymous(); } public override async Task HandleAsync(ShipDefaultBehaviorCommandRequest request, CancellationToken cancellationToken) diff --git a/apps/backend/Ships/Contracts/ShipAutomation.cs b/apps/backend/Ships/Contracts/ShipAutomation.cs new file mode 100644 index 0000000..42caaf0 --- /dev/null +++ b/apps/backend/Ships/Contracts/ShipAutomation.cs @@ -0,0 +1,19 @@ +namespace SpaceGame.Api.Ships.Contracts; + +public sealed record ShipBehaviorDefinitionSnapshot( + string Id, + string Label, + string Category, + string SupportStatus, + string Notes); + +public sealed record ShipOrderDefinitionSnapshot( + string Id, + string Label, + string Category, + string SupportStatus, + string Notes); + +public sealed record ShipAutomationCatalogSnapshot( + IReadOnlyList Behaviors, + IReadOnlyList Orders); diff --git a/apps/backend/Ships/Contracts/ShipCommands.cs b/apps/backend/Ships/Contracts/ShipCommands.cs index a7a0ef5..4972275 100644 --- a/apps/backend/Ships/Contracts/ShipCommands.cs +++ b/apps/backend/Ships/Contracts/ShipCommands.cs @@ -42,7 +42,7 @@ public sealed record ShipDefaultBehaviorCommandRequest( string? HomeStationId, string? AreaSystemId, string? TargetEntityId, - string? PreferredItemId, + string? ItemId, string? PreferredNodeId, string? PreferredConstructionSiteId, string? PreferredModuleId, diff --git a/apps/backend/Ships/Contracts/Ships.cs b/apps/backend/Ships/Contracts/Ships.cs index a7e245a..fc61257 100644 --- a/apps/backend/Ships/Contracts/Ships.cs +++ b/apps/backend/Ships/Contracts/Ships.cs @@ -10,6 +10,8 @@ public sealed record ShipSkillProfileSnapshot( public sealed record ShipOrderSnapshot( string Id, string Kind, + string SourceKind, + string SourceId, string Status, int Priority, bool InterruptCurrentPlan, @@ -53,7 +55,7 @@ public sealed record DefaultBehaviorSnapshot( string? HomeStationId, string? AreaSystemId, string? TargetEntityId, - string? PreferredItemId, + string? ItemId, string? PreferredNodeId, string? PreferredConstructionSiteId, string? PreferredModuleId, @@ -129,9 +131,9 @@ public sealed record ShipPlanSnapshot( public sealed record ShipSnapshot( string Id, - string Label, - string Kind, - string Class, + string Name, + string Purpose, + string Type, string SystemId, Vector3Dto LocalPosition, Vector3Dto LocalVelocity, @@ -164,9 +166,9 @@ public sealed record ShipSnapshot( public sealed record ShipDelta( string Id, - string Label, - string Kind, - string Class, + string Name, + string Purpose, + string Type, string SystemId, Vector3Dto LocalPosition, Vector3Dto LocalVelocity, diff --git a/apps/backend/Ships/Runtime/ShipRuntimeModels.cs b/apps/backend/Ships/Runtime/ShipRuntimeModels.cs index 1ae78c5..f4c447c 100644 --- a/apps/backend/Ships/Runtime/ShipRuntimeModels.cs +++ b/apps/backend/Ships/Runtime/ShipRuntimeModels.cs @@ -47,6 +47,8 @@ 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; @@ -75,7 +77,7 @@ public sealed class DefaultBehaviorRuntime public string? HomeStationId { get; set; } public string? AreaSystemId { get; set; } public string? TargetEntityId { get; set; } - public string? PreferredItemId { get; set; } + public string? ItemId { get; set; } public string? PreferredNodeId { get; set; } public string? PreferredConstructionSiteId { get; set; } public string? PreferredModuleId { get; set; } diff --git a/apps/backend/Ships/Simulation/ShipAiService.cs b/apps/backend/Ships/Simulation/ShipAiService.cs deleted file mode 100644 index 1b6ab52..0000000 --- a/apps/backend/Ships/Simulation/ShipAiService.cs +++ /dev/null @@ -1,2705 +0,0 @@ -using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; -using static SpaceGame.Api.Stations.Simulation.InfrastructureSimulationService; -using static SpaceGame.Api.Stations.Simulation.StationSimulationService; - -namespace SpaceGame.Api.Ships.Simulation; - -public sealed class ShipAiService( - IBalanceService balance) -{ - private const float WarpEngageDistanceKilometers = 250_000f; - private const float FrigateDps = 7f; - private const float DestroyerDps = 12f; - private const float CruiserDps = 18f; - private const float CapitalDps = 26f; - - internal void UpdateShip(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection events) - { - if (ship.ReplanCooldownSeconds > 0f) - { - ship.ReplanCooldownSeconds = MathF.Max(0f, ship.ReplanCooldownSeconds - deltaSeconds); - } - - var previousState = ship.State; - var previousPlanId = ship.ActivePlan?.Id; - var previousStepId = GetCurrentStep(ship.ActivePlan)?.Id; - - EnsurePlan(world, ship, events); - ExecutePlan(world, ship, deltaSeconds, events); - TrackHistory(ship); - EmitStateEvents(ship, previousState, previousPlanId, previousStepId, events); - } - - private void EnsurePlan(SimulationWorld world, ShipRuntime ship, ICollection events) - { - var emergencyPlan = BuildEmergencyPlan(world, ship); - if (emergencyPlan is not null) - { - ship.LastReplanReason = "rule-safety"; - ReplacePlan(ship, emergencyPlan, "rule-safety", events); - return; - } - - var topOrder = GetTopOrder(ship); - if (topOrder is not null && topOrder.Status == OrderStatus.Queued) - { - topOrder.Status = OrderStatus.Active; - } - - var desiredSourceKind = topOrder is null ? AiPlanSourceKind.DefaultBehavior : AiPlanSourceKind.Order; - var desiredSourceId = topOrder?.Id ?? ResolveBehaviorSource(world, ship).SourceId; - var currentPlan = ship.ActivePlan; - - if (currentPlan is not null - && currentPlan.Status is not AiPlanStatus.Completed and not AiPlanStatus.Failed and not AiPlanStatus.Interrupted - && currentPlan.SourceKind == desiredSourceKind - && string.Equals(currentPlan.SourceId, desiredSourceId, StringComparison.Ordinal) - && !ship.NeedsReplan) - { - return; - } - - if (ship.ReplanCooldownSeconds > 0f && currentPlan is null) - { - return; - } - - ShipPlanRuntime? nextPlan = desiredSourceKind == AiPlanSourceKind.Order - ? BuildOrderPlan(world, ship, topOrder!) - : BuildBehaviorPlan(world, ship); - - if (nextPlan is null) - { - nextPlan = CreateIdlePlan(ship, desiredSourceKind, desiredSourceId, "No viable plan"); - } - - if (nextPlan.Kind != "idle") - { - ship.LastAccessFailureReason = null; - } - - ReplacePlan(ship, nextPlan, "replanned", events); - } - - private void ExecutePlan(SimulationWorld world, ShipRuntime ship, float deltaSeconds, ICollection events) - { - var plan = ship.ActivePlan; - if (plan is null) - { - ship.State = ShipState.Idle; - ship.TargetPosition = ship.Position; - return; - } - - if (plan.CurrentStepIndex >= plan.Steps.Count) - { - CompletePlan(ship, plan, events); - return; - } - - plan.Status = AiPlanStatus.Running; - plan.UpdatedAtUtc = DateTimeOffset.UtcNow; - - var step = plan.Steps[plan.CurrentStepIndex]; - if (step.Status == AiPlanStepStatus.Planned) - { - step.Status = AiPlanStepStatus.Running; - } - - if (step.CurrentSubTaskIndex >= step.SubTasks.Count) - { - CompleteStep(plan, step); - return; - } - - var subTask = step.SubTasks[step.CurrentSubTaskIndex]; - if (subTask.Status == WorkStatus.Pending) - { - subTask.Status = WorkStatus.Active; - } - - var outcome = UpdateSubTask(world, ship, step, subTask, deltaSeconds); - switch (outcome) - { - case SubTaskOutcome.Active: - step.Status = subTask.Status == WorkStatus.Blocked ? AiPlanStepStatus.Blocked : AiPlanStepStatus.Running; - plan.Status = subTask.Status == WorkStatus.Blocked ? AiPlanStatus.Blocked : AiPlanStatus.Running; - return; - case SubTaskOutcome.Completed: - subTask.Status = WorkStatus.Completed; - subTask.Progress = 1f; - step.CurrentSubTaskIndex += 1; - step.BlockingReason = null; - if (step.CurrentSubTaskIndex >= step.SubTasks.Count) - { - CompleteStep(plan, step); - } - return; - case SubTaskOutcome.Failed: - subTask.Status = WorkStatus.Failed; - step.Status = AiPlanStepStatus.Failed; - plan.Status = AiPlanStatus.Failed; - plan.FailureReason = subTask.BlockingReason ?? "subtask-failed"; - ship.NeedsReplan = true; - ship.ReplanCooldownSeconds = 0.5f; - ship.LastReplanReason = plan.FailureReason; - return; - } - } - - private static void CompleteStep(ShipPlanRuntime plan, ShipPlanStepRuntime step) - { - step.Status = AiPlanStepStatus.Completed; - step.BlockingReason = null; - plan.CurrentStepIndex += 1; - if (plan.CurrentStepIndex >= plan.Steps.Count) - { - plan.Status = AiPlanStatus.Completed; - } - } - - private static void CompletePlan(ShipRuntime ship, ShipPlanRuntime plan, ICollection events) - { - plan.Status = AiPlanStatus.Completed; - var completedOrder = plan.SourceKind == AiPlanSourceKind.Order - ? ship.OrderQueue.FirstOrDefault(order => order.Id == plan.SourceId) - : null; - if (completedOrder is not null) - { - completedOrder.Status = OrderStatus.Completed; - ship.OrderQueue.RemoveAll(order => order.Id == completedOrder.Id); - } - else if (plan.SourceKind == AiPlanSourceKind.DefaultBehavior - && string.Equals(ship.DefaultBehavior.Kind, "repeat-orders", StringComparison.Ordinal) - && ship.DefaultBehavior.RepeatOrders.Count > 0) - { - ship.DefaultBehavior.RepeatIndex = (ship.DefaultBehavior.RepeatIndex + 1) % ship.DefaultBehavior.RepeatOrders.Count; - } - - ship.ActivePlan = null; - ship.NeedsReplan = true; - ship.ReplanCooldownSeconds = 0.25f; - ship.LastReplanReason = "plan-completed"; - events.Add(new SimulationEventRecord("ship", ship.Id, "plan-completed", $"{ship.Definition.Label} completed {plan.Kind}.", DateTimeOffset.UtcNow)); - } - - private void ReplacePlan(ShipRuntime ship, ShipPlanRuntime nextPlan, string reason, ICollection events) - { - if (ship.ActivePlan is not null && ship.ActivePlan.Status is not AiPlanStatus.Completed and not AiPlanStatus.Failed) - { - ship.ActivePlan.Status = AiPlanStatus.Interrupted; - ship.ActivePlan.InterruptReason = reason; - } - - ship.ActivePlan = nextPlan; - ship.NeedsReplan = false; - ship.ReplanCooldownSeconds = 0f; - ship.LastReplanReason = reason; - events.Add(new SimulationEventRecord("ship", ship.Id, "plan-updated", $"{ship.Definition.Label} planned {nextPlan.Kind}.", DateTimeOffset.UtcNow)); - } - - private ShipPlanRuntime? BuildEmergencyPlan(SimulationWorld world, ShipRuntime ship) - { - var policy = ResolvePolicy(world, ship.PolicySetId); - if (policy is null) - { - return null; - } - - var hullRatio = ship.Definition.MaxHealth <= 0.01f ? 1f : ship.Health / ship.Definition.MaxHealth; - if (hullRatio > policy.FleeHullRatio) - { - return null; - } - - var hostileNearby = world.Ships.Any(candidate => - candidate.Health > 0f && - candidate.FactionId != ship.FactionId && - candidate.SystemId == ship.SystemId && - candidate.Position.DistanceTo(ship.Position) <= 200f); - if (!hostileNearby) - { - return null; - } - - var safeStation = world.Stations - .Where(station => station.FactionId == ship.FactionId) - .OrderByDescending(station => station.SystemId == ship.SystemId ? 1 : 0) - .ThenBy(station => station.Position.DistanceTo(ship.Position)) - .FirstOrDefault(); - - var plan = new ShipPlanRuntime - { - Id = $"plan-{ship.Id}-safety-{Guid.NewGuid():N}", - SourceKind = AiPlanSourceKind.Rule, - SourceId = ShipOrderKinds.Flee, - Kind = "safety-flee", - Summary = "Emergency retreat", - }; - - if (safeStation is null) - { - plan.Steps.Add(CreateStep("step-flee-hold", "hold-position", "Hold position away from hostiles", - [ - CreateSubTask("sub-flee-hold", ShipTaskKinds.HoldPosition, "Hold safe position", ship.SystemId, ship.Position, null, 0f, 3f) - ])); - return plan; - } - - plan.Steps.Add(CreateStep("step-flee-travel", "travel", "Travel to safe station", - [ - CreateSubTask("sub-flee-travel", ShipTaskKinds.Travel, $"Travel to {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, MathF.Max(balance.ArrivalThreshold, safeStation.Radius + 12f), 0f) - ])); - plan.Steps.Add(CreateStep("step-flee-dock", "dock", "Dock at safe station", - [ - CreateSubTask("sub-flee-dock", ShipTaskKinds.Dock, $"Dock at {safeStation.Label}", safeStation.SystemId, safeStation.Position, safeStation.Id, 4f, 0f) - ])); - return plan; - } - - private ShipPlanRuntime? BuildOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) - { - return order.Kind switch - { - var kind when string.Equals(kind, ShipOrderKinds.Move, StringComparison.Ordinal) => BuildMovePlan(ship, order), - var kind when string.Equals(kind, ShipOrderKinds.DockAtStation, StringComparison.Ordinal) => BuildDockOrderPlan(world, ship, order), - var kind when string.Equals(kind, ShipOrderKinds.DockAndWait, StringComparison.Ordinal) => BuildDockAndWaitOrderPlan(world, ship, order), - var kind when string.Equals(kind, ShipOrderKinds.FlyAndWait, StringComparison.Ordinal) => BuildFlyAndWaitOrderPlan(ship, order), - var kind when string.Equals(kind, ShipOrderKinds.FlyToObject, StringComparison.Ordinal) => BuildFlyToObjectOrderPlan(world, ship, order), - var kind when string.Equals(kind, ShipOrderKinds.FollowShip, StringComparison.Ordinal) => BuildFollowShipOrderPlan(world, ship, order), - var kind when string.Equals(kind, ShipOrderKinds.TradeRoute, StringComparison.Ordinal) => BuildTradeOrderPlan(world, ship, order), - var kind when string.Equals(kind, ShipOrderKinds.MineAndDeliver, StringComparison.Ordinal) => BuildMineOrderPlan(world, ship, order), - var kind when string.Equals(kind, ShipOrderKinds.BuildAtSite, StringComparison.Ordinal) => BuildBuildOrderPlan(world, ship, order), - var kind when string.Equals(kind, ShipOrderKinds.AttackTarget, StringComparison.Ordinal) => BuildAttackOrderPlan(world, ship, order), - var kind when string.Equals(kind, ShipOrderKinds.HoldPosition, StringComparison.Ordinal) => BuildHoldOrderPlan(ship, order), - _ => null, - }; - } - - private ShipPlanRuntime? BuildBehaviorPlan(SimulationWorld world, ShipRuntime ship) - { - var (behaviorKind, sourceId) = ResolveBehaviorSource(world, ship); - return behaviorKind switch - { - "local-auto-mine" => BuildMiningBehaviorPlan(world, ship, "local-auto-mine", sourceId), - "advanced-auto-mine" => BuildMiningBehaviorPlan(world, ship, "advanced-auto-mine", sourceId), - "expert-auto-mine" => BuildMiningBehaviorPlan(world, ship, "expert-auto-mine", sourceId), - "local-auto-trade" => BuildTradeBehaviorPlan(world, ship, "local-auto-trade", sourceId), - "advanced-auto-trade" => BuildTradeBehaviorPlan(world, ship, "advanced-auto-trade", sourceId), - "fill-shortages" => BuildTradeBehaviorPlan(world, ship, "fill-shortages", sourceId), - "find-build-tasks" => BuildTradeBehaviorPlan(world, ship, "find-build-tasks", sourceId), - "revisit-known-stations" => BuildTradeBehaviorPlan(world, ship, "revisit-known-stations", sourceId), - "supply-fleet" => BuildTradeBehaviorPlan(world, ship, "supply-fleet", sourceId), - "construct-station" => BuildConstructionBehaviorPlan(world, ship, sourceId), - "attack-target" => BuildAttackBehaviorPlan(world, ship, sourceId), - "protect-position" => BuildProtectPositionBehaviorPlan(world, ship, sourceId), - "protect-ship" => BuildProtectShipBehaviorPlan(world, ship, sourceId), - "protect-station" => BuildProtectStationBehaviorPlan(world, ship, sourceId), - "police" => BuildPoliceBehaviorPlan(world, ship, sourceId), - "patrol" => BuildPatrolBehaviorPlan(world, ship, sourceId), - "dock-and-wait" => BuildDockAndWaitBehaviorPlan(world, ship, sourceId), - "fly-and-wait" => BuildFlyAndWaitBehaviorPlan(ship, sourceId), - "fly-to-object" => BuildFlyToObjectBehaviorPlan(world, ship, sourceId), - "follow-ship" => BuildFollowShipBehaviorPlan(world, ship, sourceId), - "hold-position" => BuildBehaviorHoldPositionPlan(ship, sourceId), - "auto-salvage" => BuildAutoSalvageBehaviorPlan(world, ship, sourceId), - "repeat-orders" => BuildRepeatOrdersBehaviorPlan(world, ship, sourceId), - _ => CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "Idle"), - }; - } - - private static (string BehaviorKind, string SourceId) ResolveBehaviorSource(SimulationWorld world, ShipRuntime ship) - { - var assignment = ResolveAssignment(world, ship); - return assignment is null - ? (ship.DefaultBehavior.Kind, ship.DefaultBehavior.Kind) - : (assignment.BehaviorKind, assignment.ObjectiveId); - } - - private ShipPlanRuntime BuildMovePlan(ShipRuntime ship, ShipOrderRuntime order) - { - var targetSystemId = order.TargetSystemId ?? ship.SystemId; - var targetPosition = order.TargetPosition ?? ship.Position; - return CreatePlan( - ship, - AiPlanSourceKind.Order, - order.Id, - "move", - order.Label ?? "Move order", - [ - CreateStep("step-move", "travel", order.Label ?? "Travel", - [ - CreateSubTask("sub-move-travel", ShipTaskKinds.Travel, order.Label ?? "Travel", targetSystemId, targetPosition, order.TargetEntityId, 0f, 0f) - ]) - ]); - } - - private ShipPlanRuntime? BuildDockOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) - { - var station = ResolveStation(world, order.TargetEntityId ?? order.DestinationStationId); - if (station is null) - { - order.FailureReason = "station-missing"; - return null; - } - - return CreatePlan( - ship, - AiPlanSourceKind.Order, - order.Id, - "dock-at-station", - order.Label ?? $"Dock at {station.Label}", - [ - CreateStep("step-dock-travel", "travel", $"Travel to {station.Label}", - [ - CreateSubTask("sub-dock-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(balance.ArrivalThreshold, station.Radius + 12f), 0f) - ]), - CreateStep("step-dock", "dock", $"Dock at {station.Label}", - [ - CreateSubTask("sub-dock", ShipTaskKinds.Dock, $"Dock at {station.Label}", station.SystemId, station.Position, station.Id, 4f, 0f) - ]) - ]); - } - - private ShipPlanRuntime? BuildTradeOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) - { - if (order.SourceStationId is null || order.DestinationStationId is null || order.ItemId is null) - { - order.FailureReason = "trade-order-incomplete"; - return null; - } - - var route = ResolveTradeRoute(world, order.ItemId, order.SourceStationId, order.DestinationStationId); - if (route is null) - { - order.FailureReason = "trade-route-missing"; - return null; - } - - return BuildTradePlan(ship, AiPlanSourceKind.Order, order.Id, route, order.Label ?? route.Summary); - } - - private ShipPlanRuntime? BuildMineOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) - { - var homeStation = ResolveStation(world, order.DestinationStationId ?? ship.DefaultBehavior.HomeStationId); - var node = ResolveNode(world, order.NodeId); - if (homeStation is null || node is null) - { - order.FailureReason = "mine-order-incomplete"; - return null; - } - - return BuildMiningPlan(ship, AiPlanSourceKind.Order, order.Id, node, homeStation, order.Label ?? $"Mine {node.ItemId}"); - } - - private ShipPlanRuntime? BuildBuildOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) - { - var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == (order.ConstructionSiteId ?? order.TargetEntityId)); - if (site is null) - { - order.FailureReason = "construction-site-missing"; - return null; - } - - var supportStation = ResolveSupportStation(world, ship, site); - if (supportStation is null) - { - order.FailureReason = "support-station-missing"; - return null; - } - - return BuildConstructionPlan(ship, AiPlanSourceKind.Order, order.Id, site, supportStation, order.Label ?? $"Build {site.BlueprintId}"); - } - - private ShipPlanRuntime? BuildAttackOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) - { - var targetId = order.TargetEntityId; - if (targetId is null) - { - order.FailureReason = "attack-target-missing"; - return null; - } - - return BuildAttackPlan(ship, AiPlanSourceKind.Order, order.Id, targetId, order.TargetSystemId, order.Label ?? "Attack target"); - } - - private ShipPlanRuntime BuildHoldOrderPlan(ShipRuntime ship, ShipOrderRuntime order) - { - return CreatePlan( - ship, - AiPlanSourceKind.Order, - order.Id, - "hold-position", - order.Label ?? "Hold position", - [ - CreateStep("step-hold", "hold-position", order.Label ?? "Hold position", - [ - CreateSubTask("sub-hold", ShipTaskKinds.HoldPosition, order.Label ?? "Hold position", order.TargetSystemId ?? ship.SystemId, order.TargetPosition ?? ship.Position, order.TargetEntityId, 0f, 3f) - ]) - ]); - } - - private ShipPlanRuntime? BuildDockAndWaitOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) - { - var station = ResolveStation(world, order.TargetEntityId ?? order.DestinationStationId ?? ship.DefaultBehavior.HomeStationId); - if (station is null) - { - order.FailureReason = "station-missing"; - return null; - } - - return BuildDockAndWaitPlan(ship, AiPlanSourceKind.Order, order.Id, station, MathF.Max(1f, order.WaitSeconds), order.Label ?? $"Dock and wait at {station.Label}"); - } - - private ShipPlanRuntime BuildFlyAndWaitOrderPlan(ShipRuntime ship, ShipOrderRuntime order) - { - var systemId = order.TargetSystemId ?? ship.SystemId; - var targetPosition = order.TargetPosition ?? ship.Position; - return BuildFlyAndWaitPlan(ship, AiPlanSourceKind.Order, order.Id, systemId, targetPosition, MathF.Max(1f, order.WaitSeconds), order.Label ?? "Fly and wait"); - } - - private ShipPlanRuntime? BuildFlyToObjectOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) - { - var targetEntityId = order.TargetEntityId; - if (targetEntityId is null) - { - order.FailureReason = "target-missing"; - return null; - } - - var objectTarget = ResolveObjectTarget(world, targetEntityId); - if (objectTarget is null) - { - order.FailureReason = "target-missing"; - return null; - } - - return BuildFlyToObjectPlan(ship, AiPlanSourceKind.Order, order.Id, objectTarget.Value.SystemId, objectTarget.Value.Position, targetEntityId, order.Label ?? $"Fly to {targetEntityId}"); - } - - private ShipPlanRuntime? BuildFollowShipOrderPlan(SimulationWorld world, ShipRuntime ship, ShipOrderRuntime order) - { - var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == order.TargetEntityId && candidate.Health > 0f); - if (targetShip is null) - { - order.FailureReason = "target-ship-missing"; - return null; - } - - return BuildFollowShipPlan(ship, AiPlanSourceKind.Order, order.Id, targetShip, MathF.Max(order.Radius, 18f), MathF.Max(2f, order.WaitSeconds), order.Label ?? $"Follow {targetShip.Definition.Label}"); - } - - private ShipPlanRuntime? BuildMiningBehaviorPlan(SimulationWorld world, ShipRuntime ship, string behaviorKind, string sourceId) - { - var assignment = ResolveAssignment(world, ship); - var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); - if (homeStation is null) - { - return CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No home station"); - } - - var opportunity = SelectMiningOpportunity(world, ship, homeStation, assignment, behaviorKind); - return opportunity is null - ? CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No mineable node") - : BuildMiningPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, opportunity.Node, opportunity.DropOffStation, opportunity.Summary); - } - - private ShipPlanRuntime BuildMiningPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ResourceNodeRuntime node, StationRuntime homeStation, string summary) - { - var extractionPosition = GetResourceHoldPosition(node.Position, ship.Id, 20f); - return CreatePlan( - ship, - sourceKind, - sourceId, - "mine-and-deliver", - summary, - [ - CreateStep("step-mine", "mine", $"Mine {node.ItemId}", - [ - CreateSubTask("sub-mine-travel", ShipTaskKinds.Travel, $"Travel to {node.ItemId} node", node.SystemId, extractionPosition, node.Id, 8f, 0f), - CreateSubTask("sub-mine", ShipTaskKinds.MineNode, $"Mine {node.ItemId}", node.SystemId, extractionPosition, node.Id, 8f, ship.Definition.CargoCapacity) - ]), - CreateStep("step-deliver", "deliver", $"Deliver {node.ItemId} to {homeStation.Label}", - [ - CreateSubTask("sub-deliver-travel", ShipTaskKinds.Travel, $"Travel to {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, MathF.Max(homeStation.Radius + 12f, 12f), 0f), - CreateSubTask("sub-deliver-dock", ShipTaskKinds.Dock, $"Dock at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 4f, 0f), - CreateSubTask("sub-deliver-unload", ShipTaskKinds.UnloadCargo, $"Unload at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 0f, ship.Definition.CargoCapacity), - CreateSubTask("sub-deliver-undock", ShipTaskKinds.Undock, $"Undock from {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, MathF.Max(4f, 12f), 0f) - ]) - ]); - } - - private ShipPlanRuntime? BuildTradeBehaviorPlan(SimulationWorld world, ShipRuntime ship, string behaviorKind, string sourceId) - { - var assignment = ResolveAssignment(world, ship); - var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); - if (string.Equals(behaviorKind, "supply-fleet", StringComparison.Ordinal)) - { - var fleetPlan = SelectFleetSupplyPlan(world, ship, homeStation); - return fleetPlan is null - ? CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No fleet to supply") - : BuildFleetSupplyPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, fleetPlan); - } - - var route = SelectTradeRoute(world, ship, homeStation, behaviorKind, ship.DefaultBehavior.KnownStationsOnly); - if (route is not null) - { - return BuildTradePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, route, route.Summary); - } - - if (string.Equals(behaviorKind, "revisit-known-stations", StringComparison.Ordinal) - && SelectKnownStationVisit(world, ship, homeStation) is { } visitStation) - { - return BuildDockAndWaitPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, visitStation, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), $"Revisit {visitStation.Label}"); - } - - return CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No trade route"); - } - - private ShipPlanRuntime BuildTradePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, TradeRoutePlan route, string summary) - { - return CreatePlan( - ship, - sourceKind, - sourceId, - "trade-route", - summary, - [ - CreateStep("step-acquire", "acquire-cargo", $"Load {route.ItemId} at {route.SourceStation.Label}", - [ - CreateSubTask("sub-acquire-travel", ShipTaskKinds.Travel, $"Travel to {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, MathF.Max(route.SourceStation.Radius + 12f, 12f), 0f), - CreateSubTask("sub-acquire-dock", ShipTaskKinds.Dock, $"Dock at {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, 4f, 0f), - CreateSubTask("sub-acquire-load", ShipTaskKinds.LoadCargo, $"Load {route.ItemId}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, 0f, ship.Definition.CargoCapacity, itemId: route.ItemId), - CreateSubTask("sub-acquire-undock", ShipTaskKinds.Undock, $"Undock from {route.SourceStation.Label}", route.SourceStation.SystemId, route.SourceStation.Position, route.SourceStation.Id, MathF.Max(4f, 12f), 0f) - ]), - CreateStep("step-deliver", "deliver-cargo", $"Deliver {route.ItemId} to {route.DestinationStation.Label}", - [ - CreateSubTask("sub-route-travel", ShipTaskKinds.Travel, $"Travel to {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, MathF.Max(route.DestinationStation.Radius + 12f, 12f), 0f), - CreateSubTask("sub-route-dock", ShipTaskKinds.Dock, $"Dock at {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, 4f, 0f), - CreateSubTask("sub-route-unload", ShipTaskKinds.UnloadCargo, $"Unload {route.ItemId}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, 0f, ship.Definition.CargoCapacity, itemId: route.ItemId), - CreateSubTask("sub-route-undock", ShipTaskKinds.Undock, $"Undock from {route.DestinationStation.Label}", route.DestinationStation.SystemId, route.DestinationStation.Position, route.DestinationStation.Id, MathF.Max(4f, 12f), 0f) - ]) - ]); - } - - private ShipPlanRuntime BuildFleetSupplyPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, FleetSupplyPlan plan) - { - return CreatePlan( - ship, - sourceKind, - sourceId, - "supply-fleet", - plan.Summary, - [ - CreateStep("step-fleet-acquire", "acquire-cargo", $"Load {plan.ItemId} at {plan.SourceStation.Label}", - [ - CreateSubTask("sub-fleet-source-travel", ShipTaskKinds.Travel, $"Travel to {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, MathF.Max(plan.SourceStation.Radius + 12f, 12f), 0f), - CreateSubTask("sub-fleet-source-dock", ShipTaskKinds.Dock, $"Dock at {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 4f, 0f), - CreateSubTask("sub-fleet-source-load", ShipTaskKinds.LoadCargo, $"Load {plan.ItemId}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 0f, plan.Amount, itemId: plan.ItemId), - CreateSubTask("sub-fleet-source-undock", ShipTaskKinds.Undock, $"Undock from {plan.SourceStation.Label}", plan.SourceStation.SystemId, plan.SourceStation.Position, plan.SourceStation.Id, 12f, 0f), - ]), - CreateStep("step-fleet-deliver", "deliver-fleet", $"Deliver {plan.ItemId} to {plan.TargetShip.Definition.Label}", - [ - CreateSubTask("sub-fleet-follow", ShipTaskKinds.FollowTarget, $"Rendezvous with {plan.TargetShip.Definition.Label}", plan.TargetShip.SystemId, plan.TargetShip.Position, plan.TargetShip.Id, plan.Radius, 6f), - CreateSubTask("sub-fleet-transfer", ShipTaskKinds.TransferCargoToShip, $"Transfer {plan.ItemId}", plan.TargetShip.SystemId, plan.TargetShip.Position, plan.TargetShip.Id, plan.Radius, plan.Amount, itemId: plan.ItemId), - ]) - ]); - } - - private ShipPlanRuntime? BuildConstructionBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var assignment = ResolveAssignment(world, ship); - var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == (assignment?.TargetEntityId ?? ship.DefaultBehavior.PreferredConstructionSiteId)) - ?? world.ConstructionSites - .Where(candidate => candidate.FactionId == ship.FactionId && candidate.State is ConstructionSiteStateKinds.Active or ConstructionSiteStateKinds.Planned) - .OrderBy(candidate => candidate.Id, StringComparer.Ordinal) - .FirstOrDefault(); - if (site is null) - { - return CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No construction site"); - } - - var supportStation = ResolveSupportStation(world, ship, site); - return supportStation is null - ? CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No support station") - : BuildConstructionPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, site, supportStation, $"Build {site.BlueprintId}"); - } - - private ShipPlanRuntime BuildConstructionPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ConstructionSiteRuntime site, StationRuntime supportStation, string summary) - { - var targetPosition = site.StationId is null ? supportStation.Position : supportStation.Position; - return CreatePlan( - ship, - sourceKind, - sourceId, - "construction-support", - summary, - [ - CreateStep("step-construction-deliver", "deliver-materials", $"Deliver materials to {site.Id}", - [ - CreateSubTask("sub-construction-travel", ShipTaskKinds.Travel, $"Travel to support station {supportStation.Label}", supportStation.SystemId, targetPosition, supportStation.Id, MathF.Max(supportStation.Radius + 18f, 18f), 0f), - CreateSubTask("sub-construction-deliver", ShipTaskKinds.DeliverConstruction, $"Deliver materials to {site.Id}", site.SystemId, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f) - ]), - CreateStep("step-construction-build", "build-site", $"Build {site.Id}", - [ - CreateSubTask("sub-construction-build", ShipTaskKinds.BuildConstructionSite, $"Build {site.Id}", site.SystemId, site.CelestialId is null ? supportStation.Position : supportStation.Position, site.Id, 12f, 0f) - ]) - ]); - } - - private ShipPlanRuntime? BuildAttackBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var assignment = ResolveAssignment(world, ship); - var targetId = assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId; - if (targetId is null) - { - return BuildPatrolBehaviorPlan(world, ship, sourceId); - } - - return BuildAttackPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, targetId, assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId, "Attack target"); - } - - private ShipPlanRuntime BuildAttackPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string? targetSystemId, string summary) - { - return CreatePlan( - ship, - sourceKind, - sourceId, - "attack-target", - summary, - [ - CreateStep("step-attack", "attack-target", summary, - [ - CreateSubTask("sub-attack", ShipTaskKinds.AttackTarget, summary, targetSystemId ?? ship.SystemId, ship.Position, targetEntityId, 26f, 0f) - ]) - ]); - } - - private ShipPlanRuntime BuildPatrolBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var assignment = ResolveAssignment(world, ship); - var patrolSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; - var protectPosition = ship.DefaultBehavior.TargetPosition ?? assignment?.TargetPosition ?? ship.Position; - var patrolThreat = SelectThreatTarget(world, ship, patrolSystemId, protectPosition, MathF.Max(60f, ship.DefaultBehavior.Radius)); - if (patrolThreat is not null) - { - return BuildAttackPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, patrolThreat.EntityId, patrolThreat.SystemId, "Patrol intercept"); - } - - var patrolPoints = ship.DefaultBehavior.PatrolPoints; - Vector3 targetPosition; - string targetSystemId; - if (patrolPoints.Count > 0) - { - var index = ship.DefaultBehavior.PatrolIndex % patrolPoints.Count; - targetPosition = patrolPoints[index]; - ship.DefaultBehavior.PatrolIndex = (index + 1) % patrolPoints.Count; - targetSystemId = ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; - } - else if (ResolveStation(world, ship.DefaultBehavior.HomeStationId ?? ResolveAssignment(world, ship)?.HomeStationId) is { } homeStation) - { - var patrolRadius = homeStation.Radius + 90f; - targetPosition = new Vector3(homeStation.Position.X + patrolRadius, homeStation.Position.Y, homeStation.Position.Z); - targetSystemId = homeStation.SystemId; - } - else - { - targetPosition = ship.Position; - targetSystemId = ship.SystemId; - } - - return CreatePlan( - ship, - AiPlanSourceKind.DefaultBehavior, - sourceId, - "patrol", - "Patrol sector", - [ - CreateStep("step-patrol-travel", "travel", "Travel patrol waypoint", - [ - CreateSubTask("sub-patrol-travel", ShipTaskKinds.Travel, "Travel patrol waypoint", targetSystemId, targetPosition, null, 10f, 0f) - ]), - CreateStep("step-patrol-hold", "hold-position", "Hold patrol waypoint", - [ - CreateSubTask("sub-patrol-hold", ShipTaskKinds.HoldPosition, "Hold patrol waypoint", targetSystemId, targetPosition, null, 0f, 2f) - ]) - ]); - } - - private ShipPlanRuntime? BuildPoliceBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var assignment = ResolveAssignment(world, ship); - var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); - var systemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? homeStation?.SystemId ?? ship.SystemId; - var areaPosition = homeStation?.Position ?? ship.DefaultBehavior.TargetPosition ?? ship.Position; - var contact = SelectPoliceContact(world, ship, systemId, areaPosition, MathF.Max(80f, ship.DefaultBehavior.Radius)); - if (contact is null) - { - return BuildPatrolBehaviorPlan(world, ship, sourceId); - } - - return contact.Engage - ? BuildAttackPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, contact.EntityId, contact.SystemId, "Police engage") - : BuildFollowPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, contact.EntityId, contact.SystemId, contact.Position, MathF.Max(14f, ship.DefaultBehavior.Radius * 0.5f), MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), "Police inspect"); - } - - private ShipPlanRuntime BuildProtectPositionBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var assignment = ResolveAssignment(world, ship); - var targetSystemId = assignment?.TargetSystemId ?? ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; - var targetPosition = ship.DefaultBehavior.TargetPosition ?? assignment?.TargetPosition ?? ship.Position; - var threat = SelectThreatTarget(world, ship, targetSystemId, targetPosition, MathF.Max(90f, ship.DefaultBehavior.Radius)); - if (threat is not null) - { - return BuildAttackPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, threat.EntityId, threat.SystemId, "Protect position"); - } - - return BuildFlyAndWaitPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, targetSystemId, targetPosition, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), "Protect position"); - } - - private ShipPlanRuntime BuildProtectShipBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var guardTarget = world.Ships.FirstOrDefault(candidate => candidate.Id == (ResolveAssignment(world, ship)?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId) && candidate.Health > 0f); - if (guardTarget is null) - { - return BuildPatrolBehaviorPlan(world, ship, sourceId); - } - - var threat = SelectThreatTarget(world, ship, guardTarget.SystemId, guardTarget.Position, MathF.Max(90f, ship.DefaultBehavior.Radius), excludeEntityId: guardTarget.Id); - if (threat is not null) - { - return BuildAttackPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, threat.EntityId, threat.SystemId, $"Protect {guardTarget.Definition.Label}"); - } - - return BuildFollowShipPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, guardTarget, MathF.Max(18f, ship.DefaultBehavior.Radius * 0.5f), MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), $"Escort {guardTarget.Definition.Label}"); - } - - private ShipPlanRuntime BuildProtectStationBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var assignment = ResolveAssignment(world, ship); - var station = ResolveStation(world, assignment?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId ?? assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); - if (station is null) - { - return BuildPatrolBehaviorPlan(world, ship, sourceId); - } - - var threat = SelectThreatTarget(world, ship, station.SystemId, station.Position, MathF.Max(station.Radius + 80f, ship.DefaultBehavior.Radius)); - if (threat is not null) - { - return BuildAttackPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, threat.EntityId, threat.SystemId, $"Protect {station.Label}"); - } - - return BuildFlyAndWaitPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, station.SystemId, GetFormationPosition(station.Position, ship.Id, MathF.Max(station.Radius + 18f, ship.DefaultBehavior.Radius)), MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), $"Guard {station.Label}"); - } - - private ShipPlanRuntime BuildDockAndWaitBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var station = ResolveStation(world, ResolveAssignment(world, ship)?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId ?? ship.DefaultBehavior.HomeStationId); - return station is null - ? CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No station to dock") - : BuildDockAndWaitPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, station, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), $"Dock and wait at {station.Label}"); - } - - private ShipPlanRuntime BuildFlyAndWaitBehaviorPlan(ShipRuntime ship, string sourceId) - { - var targetPosition = ship.DefaultBehavior.TargetPosition ?? ship.Position; - var targetSystemId = ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; - return BuildFlyAndWaitPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, targetSystemId, targetPosition, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), "Fly and wait"); - } - - private ShipPlanRuntime BuildFlyToObjectBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var targetEntityId = ResolveAssignment(world, ship)?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId; - var objectTarget = ResolveObjectTarget(world, targetEntityId); - return objectTarget is null || targetEntityId is null - ? CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No object target") - : BuildFlyToObjectPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, objectTarget.Value.SystemId, objectTarget.Value.Position, targetEntityId, "Fly to object"); - } - - private ShipPlanRuntime BuildFollowShipBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == (ResolveAssignment(world, ship)?.TargetEntityId ?? ship.DefaultBehavior.TargetEntityId) && candidate.Health > 0f); - return targetShip is null - ? CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No ship to follow") - : BuildFollowShipPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, targetShip, MathF.Max(16f, ship.DefaultBehavior.Radius), MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), $"Follow {targetShip.Definition.Label}"); - } - - private ShipPlanRuntime BuildBehaviorHoldPositionPlan(ShipRuntime ship, string sourceId) - { - var targetPosition = ship.DefaultBehavior.TargetPosition ?? ship.Position; - var targetSystemId = ship.DefaultBehavior.AreaSystemId ?? ship.SystemId; - return BuildFlyAndWaitPlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, targetSystemId, targetPosition, MathF.Max(2f, ship.DefaultBehavior.WaitSeconds), "Hold position"); - } - - private ShipPlanRuntime BuildAutoSalvageBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - var assignment = ResolveAssignment(world, ship); - var homeStation = ResolveStation(world, assignment?.HomeStationId ?? ship.DefaultBehavior.HomeStationId); - var salvage = SelectSalvageOpportunity(world, ship, homeStation); - if (salvage is null || homeStation is null) - { - return CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No salvage target"); - } - - var approach = GetFormationPosition(salvage.Wreck.Position, ship.Id, MathF.Max(8f, ship.DefaultBehavior.Radius * 0.25f)); - return CreatePlan( - ship, - AiPlanSourceKind.DefaultBehavior, - sourceId, - "auto-salvage", - salvage.Summary, - [ - CreateStep("step-salvage-collect", "salvage", $"Salvage {salvage.Wreck.ItemId}", - [ - CreateSubTask("sub-salvage-travel", ShipTaskKinds.Travel, $"Travel to wreck {salvage.Wreck.Id}", salvage.Wreck.SystemId, approach, salvage.Wreck.Id, 8f, 0f), - CreateSubTask("sub-salvage-work", ShipTaskKinds.SalvageWreck, $"Salvage {salvage.Wreck.ItemId}", salvage.Wreck.SystemId, approach, salvage.Wreck.Id, 8f, ship.Definition.CargoCapacity, itemId: salvage.Wreck.ItemId), - ]), - CreateStep("step-salvage-deliver", "deliver-salvage", $"Deliver salvage to {homeStation.Label}", - [ - CreateSubTask("sub-salvage-deliver-travel", ShipTaskKinds.Travel, $"Travel to {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, MathF.Max(homeStation.Radius + 12f, 12f), 0f), - CreateSubTask("sub-salvage-deliver-dock", ShipTaskKinds.Dock, $"Dock at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 4f, 0f), - CreateSubTask("sub-salvage-deliver-unload", ShipTaskKinds.UnloadCargo, $"Unload salvage at {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 0f, ship.Definition.CargoCapacity, itemId: salvage.Wreck.ItemId), - CreateSubTask("sub-salvage-deliver-undock", ShipTaskKinds.Undock, $"Undock from {homeStation.Label}", homeStation.SystemId, homeStation.Position, homeStation.Id, 12f, 0f), - ]) - ]); - } - - private ShipPlanRuntime BuildRepeatOrdersBehaviorPlan(SimulationWorld world, ShipRuntime ship, string sourceId) - { - if (ship.DefaultBehavior.RepeatOrders.Count == 0) - { - return CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "No repeat orders"); - } - - var template = ship.DefaultBehavior.RepeatOrders[ship.DefaultBehavior.RepeatIndex % ship.DefaultBehavior.RepeatOrders.Count]; - var syntheticOrder = new ShipOrderRuntime - { - Id = $"repeat-{ship.Id}-{ship.DefaultBehavior.RepeatIndex}", - Kind = template.Kind, - Label = template.Label, - TargetEntityId = template.TargetEntityId, - TargetSystemId = template.TargetSystemId, - TargetPosition = template.TargetPosition, - SourceStationId = template.SourceStationId, - DestinationStationId = template.DestinationStationId, - ItemId = template.ItemId, - NodeId = template.NodeId, - ConstructionSiteId = template.ConstructionSiteId, - ModuleId = template.ModuleId, - WaitSeconds = template.WaitSeconds, - Radius = template.Radius, - MaxSystemRange = template.MaxSystemRange, - KnownStationsOnly = template.KnownStationsOnly, - }; - - return BuildOrderPlan(world, ship, syntheticOrder) - ?? CreateIdlePlan(ship, AiPlanSourceKind.DefaultBehavior, sourceId, "Invalid repeat order"); - } - - private ShipPlanRuntime BuildDockAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, StationRuntime station, float waitSeconds, string summary) - { - return CreatePlan( - ship, - sourceKind, - sourceId, - "dock-and-wait", - summary, - [ - CreateStep("step-dock-wait-travel", "travel", $"Travel to {station.Label}", - [ - CreateSubTask("sub-dock-wait-travel", ShipTaskKinds.Travel, $"Travel to {station.Label}", station.SystemId, station.Position, station.Id, MathF.Max(station.Radius + 12f, 12f), 0f), - CreateSubTask("sub-dock-wait-dock", ShipTaskKinds.Dock, $"Dock at {station.Label}", station.SystemId, station.Position, station.Id, 4f, 0f), - CreateSubTask("sub-dock-wait-hold", ShipTaskKinds.HoldPosition, $"Wait at {station.Label}", station.SystemId, station.Position, station.Id, 0f, waitSeconds), - ]) - ]); - } - - private ShipPlanRuntime BuildFlyAndWaitPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, float waitSeconds, string summary) - { - return CreatePlan( - ship, - sourceKind, - sourceId, - "fly-and-wait", - summary, - [ - CreateStep("step-fly-wait", "fly-and-wait", summary, - [ - CreateSubTask("sub-fly-wait-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, null, 6f, 0f), - CreateSubTask("sub-fly-wait-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, null, 0f, waitSeconds), - ]) - ]); - } - - private ShipPlanRuntime BuildFlyToObjectPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetSystemId, Vector3 targetPosition, string targetEntityId, string summary) - { - return CreatePlan( - ship, - sourceKind, - sourceId, - "fly-to-object", - summary, - [ - CreateStep("step-fly-object", "fly-to-object", summary, - [ - CreateSubTask("sub-fly-object-travel", ShipTaskKinds.Travel, summary, targetSystemId, targetPosition, targetEntityId, 8f, 0f), - CreateSubTask("sub-fly-object-hold", ShipTaskKinds.HoldPosition, summary, targetSystemId, targetPosition, targetEntityId, 0f, MathF.Max(1f, ship.DefaultBehavior.WaitSeconds)), - ]) - ]); - } - - private ShipPlanRuntime BuildFollowShipPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, ShipRuntime targetShip, float radius, float durationSeconds, string summary) - { - return BuildFollowPlan(ship, sourceKind, sourceId, targetShip.Id, targetShip.SystemId, targetShip.Position, radius, durationSeconds, summary); - } - - private ShipPlanRuntime BuildFollowPlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string targetEntityId, string targetSystemId, Vector3 targetPosition, float radius, float durationSeconds, string summary) - { - return CreatePlan( - ship, - sourceKind, - sourceId, - "follow-ship", - summary, - [ - CreateStep("step-follow", "follow-target", summary, - [ - CreateSubTask("sub-follow", ShipTaskKinds.FollowTarget, summary, targetSystemId, targetPosition, targetEntityId, radius, durationSeconds), - ]) - ]); - } - - private ShipPlanRuntime CreateIdlePlan(ShipRuntime ship, AiPlanSourceKind sourceKind, string sourceId, string summary) - { - return CreatePlan( - ship, - sourceKind, - sourceId, - "idle", - summary, - [ - CreateStep("step-idle", "hold-position", summary, - [ - CreateSubTask("sub-idle", ShipTaskKinds.HoldPosition, summary, ship.SystemId, ship.Position, null, 0f, 1.5f) - ]) - ]); - } - - private static ShipPlanRuntime CreatePlan( - ShipRuntime ship, - AiPlanSourceKind sourceKind, - string sourceId, - string kind, - string summary, - IReadOnlyList steps) - { - var plan = new ShipPlanRuntime - { - Id = $"plan-{ship.Id}-{Guid.NewGuid():N}", - SourceKind = sourceKind, - SourceId = sourceId, - Kind = kind, - Summary = summary, - }; - plan.Steps.AddRange(steps); - return plan; - } - - private static ShipPlanStepRuntime CreateStep(string id, string kind, string summary, IReadOnlyList subTasks) - { - var step = new ShipPlanStepRuntime - { - Id = id, - Kind = kind, - Summary = summary, - }; - step.SubTasks.AddRange(subTasks); - return step; - } - - private static ShipSubTaskRuntime CreateSubTask( - string id, - string kind, - string summary, - string targetSystemId, - Vector3 targetPosition, - string? targetEntityId, - float threshold, - float amount, - string? itemId = null, - string? moduleId = null, - string? targetNodeId = null) => - new() - { - Id = id, - Kind = kind, - Summary = summary, - TargetSystemId = targetSystemId, - TargetPosition = targetPosition, - TargetEntityId = targetEntityId, - TargetNodeId = targetNodeId, - ItemId = itemId, - ModuleId = moduleId, - Threshold = threshold, - Amount = amount, - }; - - private SubTaskOutcome UpdateSubTask(SimulationWorld world, ShipRuntime ship, ShipPlanStepRuntime step, ShipSubTaskRuntime subTask, float deltaSeconds) - { - return subTask.Kind switch - { - var kind when string.Equals(kind, ShipTaskKinds.Travel, StringComparison.Ordinal) => UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: true), - var kind when string.Equals(kind, ShipTaskKinds.FollowTarget, StringComparison.Ordinal) => UpdateFollowSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.Dock, StringComparison.Ordinal) => UpdateDockSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.Undock, StringComparison.Ordinal) => UpdateUndockSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.LoadCargo, StringComparison.Ordinal) => UpdateLoadCargoSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.UnloadCargo, StringComparison.Ordinal) => UpdateUnloadCargoSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.TransferCargoToShip, StringComparison.Ordinal) => UpdateTransferCargoToShipSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.MineNode, StringComparison.Ordinal) => UpdateMineSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.SalvageWreck, StringComparison.Ordinal) => UpdateSalvageSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.DeliverConstruction, StringComparison.Ordinal) => UpdateDeliverConstructionSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.BuildConstructionSite, StringComparison.Ordinal) => UpdateBuildConstructionSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.AttackTarget, StringComparison.Ordinal) => UpdateAttackSubTask(world, ship, subTask, deltaSeconds), - var kind when string.Equals(kind, ShipTaskKinds.HoldPosition, StringComparison.Ordinal) => UpdateHoldSubTask(ship, subTask, deltaSeconds), - _ => SubTaskOutcome.Failed, - }; - } - - private SubTaskOutcome UpdateHoldSubTask(ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - ship.State = ShipState.HoldingPosition; - ship.TargetPosition = subTask.TargetPosition ?? ship.Position; - ship.Position = ship.Position.MoveToward(ship.TargetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(ship.TargetPosition))); - return AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.1f, subTask.Amount)) ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateFollowSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f); - if (targetShip is null) - { - subTask.BlockingReason = "follow-target-missing"; - return SubTaskOutcome.Failed; - } - - var desiredPosition = GetFormationPosition(targetShip.Position, ship.Id, MathF.Max(subTask.Threshold, 16f)); - subTask.TargetSystemId = targetShip.SystemId; - subTask.TargetPosition = desiredPosition; - subTask.BlockingReason = null; - if (ship.SystemId != targetShip.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 12f)) - { - return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); - } - - ship.State = ShipState.HoldingPosition; - ship.TargetPosition = desiredPosition; - ship.Position = ship.Position.MoveToward(desiredPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(desiredPosition))); - return AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.5f, subTask.Amount)) ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateTravelSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds, bool completeOnArrival) - { - if (subTask.TargetPosition is null || subTask.TargetSystemId is null) - { - subTask.BlockingReason = "travel-target-missing"; - ship.State = ShipState.Blocked; - return SubTaskOutcome.Failed; - } - - var targetPosition = ResolveCurrentTargetPosition(world, subTask); - var targetCelestial = ResolveTravelTargetCelestial(world, subTask, targetPosition); - ship.TargetPosition = targetPosition; - - if (ship.SystemId != subTask.TargetSystemId) - { - if (!HasShipCapabilities(ship.Definition, "ftl")) - { - subTask.BlockingReason = "ftl-unavailable"; - ship.State = ShipState.Blocked; - return SubTaskOutcome.Failed; - } - - var destinationEntryCelestial = ResolveSystemEntryCelestial(world, subTask.TargetSystemId); - var destinationEntryPosition = destinationEntryCelestial?.Position ?? targetPosition; - return UpdateFtlTransit(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, destinationEntryPosition, destinationEntryCelestial, completeOnArrival, targetPosition); - } - - var currentCelestial = ResolveCurrentCelestial(world, ship); - if (targetCelestial is not null - && currentCelestial is not null - && !string.Equals(currentCelestial.Id, targetCelestial.Id, StringComparison.Ordinal)) - { - if (!HasShipCapabilities(ship.Definition, "warp")) - { - return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, completeOnArrival); - } - - return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival); - } - - if (targetCelestial is not null - && ship.Position.DistanceTo(targetPosition) > WarpEngageDistanceKilometers - && HasShipCapabilities(ship.Definition, "warp")) - { - return UpdateWarpTransit(world, ship, subTask, deltaSeconds, targetPosition, targetCelestial, completeOnArrival); - } - - return UpdateLocalTravel(world, ship, subTask, deltaSeconds, subTask.TargetSystemId, targetPosition, targetCelestial, completeOnArrival); - } - - private SubTaskOutcome UpdateAttackSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - var hostileShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f); - var hostileStation = hostileShip is null - ? world.Stations.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) - : null; - if ((hostileShip is not null && hostileShip.FactionId == ship.FactionId) - || (hostileStation is not null && hostileStation.FactionId == ship.FactionId)) - { - subTask.BlockingReason = "friendly-target"; - return SubTaskOutcome.Failed; - } - - if (hostileShip is null && hostileStation is null) - { - return SubTaskOutcome.Completed; - } - - var targetSystemId = hostileShip?.SystemId ?? hostileStation!.SystemId; - var targetPosition = hostileShip?.Position ?? hostileStation!.Position; - var attackRange = hostileShip is null ? hostileStation!.Radius + 18f : 26f; - subTask.TargetSystemId = targetSystemId; - subTask.TargetPosition = targetPosition; - subTask.Threshold = attackRange; - - if (ship.SystemId != targetSystemId || ship.Position.DistanceTo(targetPosition) > attackRange) - { - return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); - } - - ship.State = ShipState.EngagingTarget; - ship.TargetPosition = targetPosition; - ship.Position = ship.Position.MoveToward(targetPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, 8f)); - var damage = GetShipDamagePerSecond(ship) * deltaSeconds * GetSkillFactor(ship.Skills.Combat); - subTask.Progress = 1f; - - if (hostileShip is not null) - { - hostileShip.Health = MathF.Max(0f, hostileShip.Health - damage); - return hostileShip.Health <= 0f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - hostileStation!.Health = MathF.Max(0f, hostileStation.Health - (damage * 0.6f)); - return hostileStation.Health <= 0f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateMineSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - var node = ResolveNode(world, subTask.TargetEntityId ?? subTask.TargetNodeId); - if (node is null || !CanExtractNode(ship, node, world)) - { - subTask.BlockingReason = "node-missing"; - ship.State = ShipState.Blocked; - return SubTaskOutcome.Failed; - } - - var targetPosition = subTask.TargetPosition ?? GetResourceHoldPosition(node.Position, ship.Id, 20f); - ship.TargetPosition = targetPosition; - if (ship.Position.DistanceTo(targetPosition) > MathF.Max(subTask.Threshold, 8f)) - { - ship.State = ShipState.MiningApproach; - ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return SubTaskOutcome.Active; - } - - var cargoAmount = GetShipCargoAmount(ship); - if (cargoAmount >= ship.Definition.CargoCapacity - 0.01f) - { - return SubTaskOutcome.Completed; - } - - ship.State = ShipState.Mining; - if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.MiningCycleSeconds)) - { - return SubTaskOutcome.Active; - } - - var remainingCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - cargoAmount); - var mined = MathF.Min(balance.MiningRate * GetSkillFactor(ship.Skills.Mining), remainingCapacity); - mined = MathF.Min(mined, node.OreRemaining); - if (mined <= 0.01f) - { - return SubTaskOutcome.Completed; - } - - AddInventory(ship.Inventory, node.ItemId, mined); - node.OreRemaining = MathF.Max(0f, node.OreRemaining - mined); - if (GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f || node.OreRemaining <= 0.01f) - { - return SubTaskOutcome.Completed; - } - - subTask.ElapsedSeconds = 0f; - return SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateDockSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - var station = ResolveStation(world, subTask.TargetEntityId); - if (station is null) - { - subTask.BlockingReason = "dock-target-missing"; - ship.State = ShipState.Blocked; - return SubTaskOutcome.Failed; - } - - var padIndex = ship.AssignedDockingPadIndex ?? ReserveDockingPad(station, ship.Id); - if (padIndex is null) - { - ship.State = ShipState.AwaitingDock; - ship.TargetPosition = GetDockingHoldPosition(station, ship.Id); - if (ship.Position.DistanceTo(ship.TargetPosition) > 4f) - { - ship.Position = ship.Position.MoveToward(ship.TargetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - } - - subTask.Status = WorkStatus.Blocked; - subTask.BlockingReason = "waiting-for-pad"; - return SubTaskOutcome.Active; - } - - subTask.Status = WorkStatus.Active; - subTask.BlockingReason = null; - ship.AssignedDockingPadIndex = padIndex; - var padPosition = GetDockingPadPosition(station, padIndex.Value); - ship.TargetPosition = padPosition; - if (ship.Position.DistanceTo(padPosition) > 4f) - { - ship.State = ShipState.DockingApproach; - ship.Position = ship.Position.MoveToward(padPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return SubTaskOutcome.Active; - } - - ship.State = ShipState.Docking; - if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.DockingDuration)) - { - return SubTaskOutcome.Active; - } - - ship.State = ShipState.Docked; - ship.DockedStationId = station.Id; - station.DockedShipIds.Add(ship.Id); - ship.KnownStationIds.Add(station.Id); - ship.Position = padPosition; - ship.TargetPosition = padPosition; - return SubTaskOutcome.Completed; - } - - private SubTaskOutcome UpdateUndockSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - if (ship.DockedStationId is null) - { - return SubTaskOutcome.Completed; - } - - var station = ResolveStation(world, ship.DockedStationId); - if (station is null) - { - ship.DockedStationId = null; - ship.AssignedDockingPadIndex = null; - return SubTaskOutcome.Completed; - } - - var undockTarget = GetUndockTargetPosition(station, ship.AssignedDockingPadIndex, balance.UndockDistance); - ship.TargetPosition = undockTarget; - ship.State = ShipState.Undocking; - if (!AdvanceTimedSubTask(subTask, deltaSeconds, balance.UndockingDuration)) - { - ship.Position = GetShipDockedPosition(ship, station); - return SubTaskOutcome.Active; - } - - ship.Position = ship.Position.MoveToward(undockTarget, balance.UndockDistance); - if (ship.Position.DistanceTo(undockTarget) > MathF.Max(subTask.Threshold, 4f)) - { - return SubTaskOutcome.Active; - } - - station.DockedShipIds.Remove(ship.Id); - ReleaseDockingPad(station, ship.Id); - ship.DockedStationId = null; - ship.AssignedDockingPadIndex = null; - return SubTaskOutcome.Completed; - } - - private SubTaskOutcome UpdateLoadCargoSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - if (ship.DockedStationId is null) - { - subTask.BlockingReason = "not-docked"; - return SubTaskOutcome.Failed; - } - - var station = ResolveStation(world, ship.DockedStationId); - if (station is null) - { - subTask.BlockingReason = "station-missing"; - return SubTaskOutcome.Failed; - } - - ship.TargetPosition = GetShipDockedPosition(ship, station); - ship.Position = ship.TargetPosition; - ship.State = ShipState.Loading; - var itemId = subTask.ItemId; - if (itemId is null) - { - return SubTaskOutcome.Completed; - } - - var desiredAmount = subTask.Amount > 0f ? subTask.Amount : ship.Definition.CargoCapacity; - var availableCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)); - var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Trade); - var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(availableCapacity, GetInventoryAmount(station.Inventory, itemId))); - if (moved > 0.01f) - { - RemoveInventory(station.Inventory, itemId, moved); - AddInventory(ship.Inventory, itemId, moved); - } - - var loadedAmount = GetInventoryAmount(ship.Inventory, itemId); - subTask.Progress = desiredAmount <= 0.01f ? 1f : Math.Clamp(loadedAmount / desiredAmount, 0f, 1f); - return availableCapacity <= 0.01f || GetInventoryAmount(station.Inventory, itemId) <= 0.01f || loadedAmount >= desiredAmount - 0.01f - ? SubTaskOutcome.Completed - : SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateUnloadCargoSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - if (ship.DockedStationId is null) - { - subTask.BlockingReason = "not-docked"; - return SubTaskOutcome.Failed; - } - - var station = ResolveStation(world, ship.DockedStationId); - if (station is null) - { - subTask.BlockingReason = "station-missing"; - return SubTaskOutcome.Failed; - } - - ship.TargetPosition = GetShipDockedPosition(ship, station); - ship.Position = ship.TargetPosition; - ship.State = ShipState.Transferring; - var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Mining)); - - if (subTask.ItemId is not null) - { - var moved = MathF.Min(transferRate * deltaSeconds, GetInventoryAmount(ship.Inventory, subTask.ItemId)); - var accepted = TryAddStationInventory(world, station, subTask.ItemId, moved); - RemoveInventory(ship.Inventory, subTask.ItemId, accepted); - subTask.Progress = subTask.Amount <= 0.01f - ? 1f - : Math.Clamp(1f - (GetInventoryAmount(ship.Inventory, subTask.ItemId) / subTask.Amount), 0f, 1f); - return GetInventoryAmount(ship.Inventory, subTask.ItemId) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - foreach (var (itemId, amount) in ship.Inventory.ToList().OrderBy(entry => entry.Key, StringComparer.Ordinal)) - { - var moved = MathF.Min(amount, transferRate * deltaSeconds); - var accepted = TryAddStationInventory(world, station, itemId, moved); - RemoveInventory(ship.Inventory, itemId, accepted); - if (accepted > 0.01f) - { - return GetShipCargoAmount(ship) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - } - - return GetShipCargoAmount(ship) <= 0.01f ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateTransferCargoToShipSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - var targetShip = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.Health > 0f); - if (targetShip is null) - { - subTask.BlockingReason = "target-ship-missing"; - return SubTaskOutcome.Failed; - } - - var desiredPosition = GetFormationPosition(targetShip.Position, ship.Id, MathF.Max(subTask.Threshold, 12f)); - subTask.TargetSystemId = targetShip.SystemId; - subTask.TargetPosition = desiredPosition; - if (ship.SystemId != targetShip.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 12f)) - { - return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); - } - - ship.State = ShipState.Transferring; - ship.TargetPosition = desiredPosition; - ship.Position = ship.Position.MoveToward(desiredPosition, MathF.Min(GetLocalTravelSpeed(ship) * deltaSeconds, ship.Position.DistanceTo(desiredPosition))); - if (subTask.ItemId is null) - { - return SubTaskOutcome.Completed; - } - - var targetCapacity = MathF.Max(0f, targetShip.Definition.CargoCapacity - GetShipCargoAmount(targetShip)); - if (targetCapacity <= 0.01f) - { - subTask.BlockingReason = "target-cargo-full"; - return SubTaskOutcome.Failed; - } - - var transferRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Trade, ship.Skills.Navigation)); - var desiredAmount = subTask.Amount > 0f ? subTask.Amount : GetInventoryAmount(ship.Inventory, subTask.ItemId); - var moved = MathF.Min(transferRate * deltaSeconds, MathF.Min(targetCapacity, GetInventoryAmount(ship.Inventory, subTask.ItemId))); - if (moved > 0.01f) - { - RemoveInventory(ship.Inventory, subTask.ItemId, moved); - AddInventory(targetShip.Inventory, subTask.ItemId, moved); - } - - var remaining = GetInventoryAmount(ship.Inventory, subTask.ItemId); - subTask.Progress = desiredAmount <= 0.01f ? 1f : Math.Clamp(1f - (remaining / desiredAmount), 0f, 1f); - return remaining <= 0.01f || GetShipCargoAmount(targetShip) >= targetShip.Definition.CargoCapacity - 0.01f - ? SubTaskOutcome.Completed - : SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateSalvageSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId && candidate.RemainingAmount > 0.01f); - if (wreck is null) - { - return SubTaskOutcome.Completed; - } - - var desiredPosition = subTask.TargetPosition ?? GetFormationPosition(wreck.Position, ship.Id, 8f); - ship.TargetPosition = desiredPosition; - if (ship.SystemId != wreck.SystemId || ship.Position.DistanceTo(desiredPosition) > MathF.Max(subTask.Threshold, 8f)) - { - subTask.TargetSystemId = wreck.SystemId; - subTask.TargetPosition = desiredPosition; - return UpdateTravelSubTask(world, ship, subTask, deltaSeconds, completeOnArrival: false); - } - - ship.State = ShipState.Transferring; - var remainingCapacity = MathF.Max(0f, ship.Definition.CargoCapacity - GetShipCargoAmount(ship)); - if (remainingCapacity <= 0.01f) - { - return SubTaskOutcome.Completed; - } - - if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(0.4f, balance.MiningCycleSeconds * 0.8f))) - { - return SubTaskOutcome.Active; - } - - var salvageRate = balance.TransferRate * GetSkillFactor(Math.Max(ship.Skills.Mining, ship.Skills.Trade)); - var recovered = MathF.Min(salvageRate, MathF.Min(remainingCapacity, wreck.RemainingAmount)); - if (recovered > 0.01f) - { - AddInventory(ship.Inventory, wreck.ItemId, recovered); - wreck.RemainingAmount = MathF.Max(0f, wreck.RemainingAmount - recovered); - } - - if (wreck.RemainingAmount <= 0.01f) - { - world.Wrecks.RemoveAll(candidate => candidate.Id == wreck.Id); - } - - subTask.ElapsedSeconds = 0f; - return wreck.RemainingAmount <= 0.01f || GetShipCargoAmount(ship) >= ship.Definition.CargoCapacity - 0.01f - ? SubTaskOutcome.Completed - : SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateDeliverConstructionSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); - var station = site is null ? null : ResolveSupportStation(world, ship, site); - if (site is null || station is null || site.State is ConstructionSiteStateKinds.Completed or ConstructionSiteStateKinds.Destroyed) - { - subTask.BlockingReason = "construction-target-missing"; - return SubTaskOutcome.Failed; - } - - var supportPosition = ResolveSupportPosition(ship, station, site, world); - if (!IsShipWithinSupportRange(ship, supportPosition, MathF.Max(12f, subTask.Threshold))) - { - ship.State = ShipState.LocalFlight; - ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return SubTaskOutcome.Active; - } - - ship.TargetPosition = supportPosition; - ship.Position = supportPosition; - ship.State = ShipState.DeliveringConstruction; - var transferRate = balance.TransferRate * GetSkillFactor(ship.Skills.Construction); - foreach (var required in site.RequiredItems.OrderBy(entry => entry.Key, StringComparer.Ordinal)) - { - var delivered = GetInventoryAmount(site.DeliveredItems, required.Key); - var remaining = MathF.Max(0f, required.Value - delivered); - if (remaining <= 0.01f) - { - continue; - } - - var available = MathF.Max(0f, GetInventoryAmount(station.Inventory, required.Key) - GetStationReserveFloor(world, station, required.Key)); - var moved = MathF.Min(remaining, MathF.Min(available, transferRate * deltaSeconds)); - if (moved <= 0.01f) - { - continue; - } - - RemoveInventory(station.Inventory, required.Key, moved); - AddInventory(site.Inventory, required.Key, moved); - AddInventory(site.DeliveredItems, required.Key, moved); - break; - } - - subTask.Progress = site.RequiredItems.Count == 0 - ? 1f - : site.RequiredItems.Sum(required => - required.Value <= 0.01f - ? 1f - : Math.Clamp(GetInventoryAmount(site.DeliveredItems, required.Key) / required.Value, 0f, 1f)) / site.RequiredItems.Count; - return IsConstructionSiteReady(world, site) ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateBuildConstructionSubTask(SimulationWorld world, ShipRuntime ship, ShipSubTaskRuntime subTask, float deltaSeconds) - { - var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); - var station = site is null ? null : ResolveSupportStation(world, ship, site); - if (site is null || station is null || site.BlueprintId is null || site.State is ConstructionSiteStateKinds.Completed or ConstructionSiteStateKinds.Destroyed) - { - subTask.BlockingReason = "construction-site-missing"; - return SubTaskOutcome.Failed; - } - - var supportPosition = ResolveSupportPosition(ship, station, site, world); - if (!IsShipWithinSupportRange(ship, supportPosition, MathF.Max(12f, subTask.Threshold))) - { - ship.State = ShipState.LocalFlight; - ship.TargetPosition = supportPosition; - ship.Position = ship.Position.MoveToward(supportPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return SubTaskOutcome.Active; - } - - if (!IsConstructionSiteReady(world, site) || !world.ModuleRecipes.TryGetValue(site.BlueprintId, out var recipe)) - { - ship.State = ShipState.WaitingMaterials; - subTask.Status = WorkStatus.Blocked; - subTask.BlockingReason = "waiting-materials"; - return SubTaskOutcome.Active; - } - - subTask.Status = WorkStatus.Active; - subTask.BlockingReason = null; - ship.TargetPosition = supportPosition; - ship.Position = supportPosition; - ship.State = ShipState.Constructing; - site.AssignedConstructorShipIds.Add(ship.Id); - site.Progress += deltaSeconds * GetSkillFactor(ship.Skills.Construction); - subTask.Progress = recipe.Duration <= 0.01f ? 1f : Math.Clamp(site.Progress / recipe.Duration, 0f, 1f); - if (site.Progress < recipe.Duration) - { - return SubTaskOutcome.Active; - } - - if (site.StationId is null) - { - CompleteStationFoundation(world, station, site); - } - else - { - AddStationModule(world, station, site.BlueprintId); - PrepareNextConstructionSiteStep(world, station, site); - } - - site.State = ConstructionSiteStateKinds.Completed; - return SubTaskOutcome.Completed; - } - - private static bool AdvanceTimedSubTask(ShipSubTaskRuntime subTask, float deltaSeconds, float requiredSeconds) - { - subTask.TotalSeconds = requiredSeconds; - subTask.ElapsedSeconds += deltaSeconds; - subTask.Progress = requiredSeconds <= 0.01f ? 1f : Math.Clamp(subTask.ElapsedSeconds / requiredSeconds, 0f, 1f); - if (subTask.ElapsedSeconds < requiredSeconds) - { - return false; - } - - subTask.ElapsedSeconds = 0f; - return true; - } - - private SubTaskOutcome UpdateLocalTravel( - SimulationWorld world, - ShipRuntime ship, - ShipSubTaskRuntime subTask, - float deltaSeconds, - string targetSystemId, - Vector3 targetPosition, - CelestialRuntime? targetCelestial, - bool completeOnArrival) - { - var distance = ship.Position.DistanceTo(targetPosition); - ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; - ship.SpatialState.Transit = null; - ship.SpatialState.DestinationNodeId = targetCelestial?.Id; - subTask.Progress = Math.Clamp(1f - (distance / MathF.Max(distance + GetLocalTravelSpeed(ship), 1f)), 0f, 1f); - - if (distance <= MathF.Max(subTask.Threshold, balance.ArrivalThreshold)) - { - ship.Position = targetPosition; - ship.TargetPosition = targetPosition; - ship.SystemId = targetSystemId; - ship.SpatialState.CurrentSystemId = targetSystemId; - ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; - ship.State = ShipState.Arriving; - return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - ship.State = ShipState.LocalFlight; - ship.Position = ship.Position.MoveToward(targetPosition, GetLocalTravelSpeed(ship) * deltaSeconds); - return SubTaskOutcome.Active; - } - - private SubTaskOutcome UpdateWarpTransit( - SimulationWorld world, - ShipRuntime ship, - ShipSubTaskRuntime subTask, - float deltaSeconds, - Vector3 targetPosition, - CelestialRuntime targetCelestial, - bool completeOnArrival) - { - var transit = ship.SpatialState.Transit; - if (transit is null || transit.Regime != MovementRegimeKind.Warp || transit.DestinationNodeId != targetCelestial.Id) - { - transit = new ShipTransitRuntime - { - Regime = MovementRegimeKind.Warp, - OriginNodeId = ship.SpatialState.CurrentCelestialId, - DestinationNodeId = targetCelestial.Id, - StartedAtUtc = world.GeneratedAtUtc, - }; - ship.SpatialState.Transit = transit; - subTask.ElapsedSeconds = 0f; - } - - ship.SpatialState.SpaceLayer = SpaceLayerKind.SystemSpace; - ship.SpatialState.MovementRegime = MovementRegimeKind.Warp; - ship.SpatialState.CurrentCelestialId = null; - ship.SpatialState.DestinationNodeId = targetCelestial.Id; - - var spoolDuration = MathF.Max(0.4f, ship.Definition.SpoolTime * 0.5f); - if (ship.State != ShipState.Warping) - { - ship.State = ShipState.SpoolingWarp; - if (!AdvanceTimedSubTask(subTask, deltaSeconds, spoolDuration)) - { - return SubTaskOutcome.Active; - } - - ship.State = ShipState.Warping; - } - - var totalDistance = MathF.Max(0.001f, transit.OriginNodeId is null - ? ship.Position.DistanceTo(targetPosition) - : (world.Celestials.FirstOrDefault(candidate => candidate.Id == transit.OriginNodeId)?.Position.DistanceTo(targetPosition) ?? ship.Position.DistanceTo(targetPosition))); - ship.Position = ship.Position.MoveToward(targetPosition, GetWarpTravelSpeed(ship) * deltaSeconds); - transit.Progress = MathF.Min(1f, 1f - (ship.Position.DistanceTo(targetPosition) / totalDistance)); - subTask.Progress = transit.Progress; - if (ship.Position.DistanceTo(targetPosition) > 18f) - { - return SubTaskOutcome.Active; - } - - return CompleteTransitArrival(ship, subTask.TargetSystemId ?? ship.SystemId, targetPosition, targetCelestial, completeOnArrival); - } - - private SubTaskOutcome UpdateFtlTransit( - SimulationWorld world, - ShipRuntime ship, - ShipSubTaskRuntime subTask, - float deltaSeconds, - string targetSystemId, - Vector3 entryPosition, - CelestialRuntime? targetCelestial, - bool completeOnArrival, - Vector3 finalTargetPosition) - { - var destinationNodeId = targetCelestial?.Id; - var transit = ship.SpatialState.Transit; - if (transit is null || transit.Regime != MovementRegimeKind.FtlTransit || transit.DestinationNodeId != destinationNodeId) - { - transit = new ShipTransitRuntime - { - Regime = MovementRegimeKind.FtlTransit, - OriginNodeId = ship.SpatialState.CurrentCelestialId, - DestinationNodeId = destinationNodeId, - StartedAtUtc = world.GeneratedAtUtc, - }; - ship.SpatialState.Transit = transit; - subTask.ElapsedSeconds = 0f; - } - - ship.SpatialState.SpaceLayer = SpaceLayerKind.GalaxySpace; - ship.SpatialState.MovementRegime = MovementRegimeKind.FtlTransit; - ship.SpatialState.CurrentCelestialId = null; - ship.SpatialState.DestinationNodeId = destinationNodeId; - - if (ship.State != ShipState.Ftl) - { - ship.State = ShipState.SpoolingFtl; - if (!AdvanceTimedSubTask(subTask, deltaSeconds, MathF.Max(ship.Definition.SpoolTime, 0.1f))) - { - return SubTaskOutcome.Active; - } - - ship.State = ShipState.Ftl; - } - - var originSystemPosition = ResolveSystemGalaxyPosition(world, ship.SystemId); - var destinationSystemPosition = ResolveSystemGalaxyPosition(world, targetSystemId); - var totalDistance = MathF.Max(0.001f, originSystemPosition.DistanceTo(destinationSystemPosition)); - transit.Progress = MathF.Min(1f, transit.Progress + ((ship.Definition.FtlSpeed * GetSkillFactor(ship.Skills.Navigation)) * deltaSeconds / totalDistance)); - subTask.Progress = transit.Progress; - if (transit.Progress < 0.999f) - { - return SubTaskOutcome.Active; - } - - ship.Position = entryPosition; - ship.TargetPosition = finalTargetPosition; - ship.SystemId = targetSystemId; - ship.SpatialState.CurrentSystemId = targetSystemId; - ship.SpatialState.Transit = null; - ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; - ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; - ship.SpatialState.DestinationNodeId = targetCelestial?.Id; - ship.State = ShipState.Arriving; - return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - private static SubTaskOutcome CompleteTransitArrival(ShipRuntime ship, string targetSystemId, Vector3 targetPosition, CelestialRuntime? targetCelestial, bool completeOnArrival) - { - ship.Position = targetPosition; - ship.TargetPosition = targetPosition; - ship.SystemId = targetSystemId; - ship.SpatialState.CurrentSystemId = targetSystemId; - ship.SpatialState.Transit = null; - ship.SpatialState.SpaceLayer = SpaceLayerKind.LocalSpace; - ship.SpatialState.MovementRegime = MovementRegimeKind.LocalFlight; - ship.SpatialState.CurrentCelestialId = targetCelestial?.Id; - ship.SpatialState.DestinationNodeId = targetCelestial?.Id; - ship.State = ShipState.Arriving; - return completeOnArrival ? SubTaskOutcome.Completed : SubTaskOutcome.Active; - } - - private static Vector3 ResolveCurrentTargetPosition(SimulationWorld world, ShipSubTaskRuntime subTask) - { - if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId)) - { - var ship = world.Ships.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); - if (ship is not null) - { - return ship.Position; - } - - var station = ResolveStation(world, subTask.TargetEntityId); - if (station is not null) - { - return station.Position; - } - - var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); - if (celestial is not null) - { - return celestial.Position; - } - - var wreck = world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); - if (wreck is not null) - { - return wreck.Position; - } - } - - return subTask.TargetPosition ?? Vector3.Zero; - } - - private static CelestialRuntime? ResolveTravelTargetCelestial(SimulationWorld world, ShipSubTaskRuntime subTask, Vector3 targetPosition) - { - if (!string.IsNullOrWhiteSpace(subTask.TargetEntityId)) - { - var station = ResolveStation(world, subTask.TargetEntityId); - if (station?.CelestialId is not null) - { - return world.Celestials.FirstOrDefault(candidate => candidate.Id == station.CelestialId); - } - - var site = world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); - if (site?.CelestialId is not null) - { - return world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId); - } - - var celestial = world.Celestials.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId); - if (celestial is not null) - { - return celestial; - } - - if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == subTask.TargetEntityId) is { } wreck) - { - return world.Celestials - .Where(candidate => candidate.SystemId == wreck.SystemId) - .OrderBy(candidate => candidate.Position.DistanceTo(wreck.Position)) - .FirstOrDefault(); - } - } - - return world.Celestials - .Where(candidate => subTask.TargetSystemId is null || candidate.SystemId == subTask.TargetSystemId) - .OrderBy(candidate => candidate.Position.DistanceTo(targetPosition)) - .FirstOrDefault(); - } - - private static CelestialRuntime? ResolveCurrentCelestial(SimulationWorld world, ShipRuntime ship) - { - if (ship.SpatialState.CurrentCelestialId is not null) - { - return world.Celestials.FirstOrDefault(candidate => candidate.Id == ship.SpatialState.CurrentCelestialId); - } - - return world.Celestials - .Where(candidate => candidate.SystemId == ship.SystemId) - .OrderBy(candidate => candidate.Position.DistanceTo(ship.Position)) - .FirstOrDefault(); - } - - private static CelestialRuntime? ResolveSystemEntryCelestial(SimulationWorld world, string systemId) => - world.Celestials.FirstOrDefault(candidate => candidate.SystemId == systemId && candidate.Kind == SpatialNodeKind.Star); - - private static Vector3 ResolveSystemGalaxyPosition(SimulationWorld world, string systemId) => - world.Systems.FirstOrDefault(candidate => candidate.Definition.Id == systemId)?.Position ?? Vector3.Zero; - - private static float GetLocalTravelSpeed(ShipRuntime ship) => - SimulationUnits.MetersPerSecondToKilometersPerSecond(ship.Definition.Speed) * GetSkillFactor(ship.Skills.Navigation); - - private static float GetWarpTravelSpeed(ShipRuntime ship) => - SimulationUnits.AuPerSecondToKilometersPerSecond(ship.Definition.WarpSpeed) * GetSkillFactor(ship.Skills.Navigation); - - private static float GetSkillFactor(int skillLevel) => - Math.Clamp(1f + ((skillLevel - 3) * 0.08f), 0.75f, 1.4f); - - private static int GetEffectiveSkillLevel( - SimulationWorld world, - ShipRuntime ship, - Func captainSelector, - Func managerSelector) - { - var captainLevel = captainSelector(ship.Skills); - if (ship.CommanderId is null) - { - return captainLevel; - } - - var shipCommander = world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId); - var manager = shipCommander?.ParentCommanderId is null - ? shipCommander - : world.Commanders.FirstOrDefault(candidate => candidate.Id == shipCommander.ParentCommanderId) ?? shipCommander; - return Math.Clamp((captainLevel + (manager is null ? 3 : managerSelector(manager.Skills)) + 1) / 2, 1, 5); - } - - private static int ResolveBehaviorSystemRange(SimulationWorld world, ShipRuntime ship, string behaviorKind, int explicitRange) - { - if (explicitRange > 0) - { - return explicitRange; - } - - var tradeSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Trade, skills => skills.Coordination); - var miningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination); - var combatSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Combat, skills => skills.Strategy); - return behaviorKind switch - { - "local-auto-mine" or "local-auto-trade" => 0, - "advanced-auto-mine" => Math.Clamp(1 + ((miningSkill - 1) / 2), 1, 3), - "advanced-auto-trade" => Math.Clamp(1 + ((tradeSkill - 1) / 2), 1, 3), - "expert-auto-mine" => Math.Clamp(2 + ((miningSkill - 1) / 2), 2, Math.Max(world.Systems.Count - 1, 2)), - "fill-shortages" or "find-build-tasks" or "revisit-known-stations" or "supply-fleet" => Math.Clamp(1 + ((tradeSkill + 1) / 2), 1, Math.Max(world.Systems.Count - 1, 1)), - "patrol" or "police" or "protect-position" or "protect-ship" or "protect-station" => Math.Clamp(1 + ((combatSkill - 1) / 2), 1, Math.Max(world.Systems.Count - 1, 1)), - _ => Math.Max(world.Systems.Count - 1, 0), - }; - } - - private static int GetSystemDistanceTier(SimulationWorld world, string originSystemId, string targetSystemId) - { - if (string.Equals(originSystemId, targetSystemId, StringComparison.Ordinal)) - { - return 0; - } - - var originPosition = ResolveSystemGalaxyPosition(world, originSystemId); - return world.Systems - .OrderBy(system => system.Position.DistanceTo(originPosition)) - .ThenBy(system => system.Definition.Id, StringComparer.Ordinal) - .Select(system => system.Definition.Id) - .TakeWhile(systemId => !string.Equals(systemId, targetSystemId, StringComparison.Ordinal)) - .Count(); - } - - private static bool IsWithinSystemRange(SimulationWorld world, string originSystemId, string targetSystemId, int maxRange) => - maxRange < 0 || GetSystemDistanceTier(world, originSystemId, targetSystemId) <= maxRange; - - private static float GetShipDamagePerSecond(ShipRuntime ship) => - ship.Definition.Class switch - { - "frigate" => FrigateDps, - "destroyer" => DestroyerDps, - "cruiser" => CruiserDps, - "capital" => CapitalDps, - _ => 4f, - }; - - private static MiningOpportunity? SelectMiningOpportunity( - SimulationWorld world, - ShipRuntime ship, - StationRuntime homeStation, - CommanderAssignmentRuntime? assignment, - string behaviorKind) - { - var policy = ResolvePolicy(world, ship.PolicySetId); - var preferredItemId = assignment?.ItemId ?? ship.DefaultBehavior.PreferredItemId; - var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange); - var effectiveMiningSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Mining, skills => skills.Coordination); - string? deniedReason = null; - var opportunity = world.Nodes - .Where(node => - { - if (node.OreRemaining <= 0.01f || !CanExtractNode(ship, node, world) || (preferredItemId is not null && !string.Equals(node.ItemId, preferredItemId, StringComparison.Ordinal))) - { - return false; - } - - if (!TryCheckSystemAllowed(world, policy, ship.FactionId, node.SystemId, "military", out var reason)) - { - deniedReason ??= reason; - return false; - } - - return IsWithinSystemRange(world, homeStation.SystemId, node.SystemId, rangeBudget); - }) - .Select(node => - { - var buyer = SelectBestDeliveryStation(world, ship, node.ItemId, homeStation, behaviorKind); - var demandScore = GetFactionDemandScore(world, ship.FactionId, node.ItemId); - var distancePenalty = GetSystemDistanceTier(world, homeStation.SystemId, node.SystemId) * 18f; - var routeRiskPenalty = GeopoliticalSimulationService.GetSystemRouteRisk(world, node.SystemId, ship.FactionId) * 30f; - var score = (node.SystemId == homeStation.SystemId ? 55f : 0f) - + (node.OreRemaining * 0.025f) - + (demandScore * (string.Equals(behaviorKind, "expert-auto-mine", StringComparison.Ordinal) ? 22f : 12f)) - + (effectiveMiningSkill * 10f) - - distancePenalty - - routeRiskPenalty - - node.Position.DistanceTo(ship.Position); - return new MiningOpportunity(node, buyer, score, $"Mine {node.ItemId} for {buyer.Label}"); - }) - .OrderByDescending(candidate => candidate.Score) - .ThenBy(candidate => candidate.Node.Id, StringComparer.Ordinal) - .FirstOrDefault(); - if (opportunity is null && deniedReason is not null) - { - ship.LastAccessFailureReason = deniedReason; - } - - return opportunity; - } - - private static TradeRoutePlan? SelectTradeRoute( - SimulationWorld world, - ShipRuntime ship, - StationRuntime? homeStation, - string behaviorKind, - bool knownStationsOnly) - { - var policy = ResolvePolicy(world, ship.PolicySetId); - var stationsById = world.Stations - .Where(station => station.FactionId == ship.FactionId) - .ToDictionary(station => station.Id, StringComparer.Ordinal); - var originSystemId = homeStation?.SystemId ?? ship.SystemId; - var rangeBudget = ResolveBehaviorSystemRange(world, ship, behaviorKind, ship.DefaultBehavior.MaxSystemRange); - var effectiveTradeSkill = GetEffectiveSkillLevel(world, ship, skills => skills.Trade, skills => skills.Coordination); - var requireKnownStations = knownStationsOnly || string.Equals(behaviorKind, "revisit-known-stations", StringComparison.Ordinal); - string? deniedReason = null; - - var route = world.MarketOrders - .Where(order => - order.FactionId == ship.FactionId && - order.Kind == MarketOrderKinds.Buy && - order.RemainingAmount > 0.01f) - .Select(order => - { - StationRuntime? destination = null; - ConstructionSiteRuntime? destinationSite = null; - if (order.StationId is not null && stationsById.TryGetValue(order.StationId, out var destinationStation)) - { - destination = destinationStation; - } - else if (order.ConstructionSiteId is not null) - { - destinationSite = world.ConstructionSites.FirstOrDefault(site => site.Id == order.ConstructionSiteId); - if (destinationSite is not null) - { - destination = ResolveSupportStation(world, ship, destinationSite); - } - } - - if (destination is null) - { - return null; - } - - if (!TryCheckSystemAllowed(world, policy, ship.FactionId, destination.SystemId, "trade", out var destinationDeniedReason)) - { - deniedReason ??= destinationDeniedReason; - return null; - } - if (!IsWithinSystemRange(world, originSystemId, destination.SystemId, rangeBudget)) - { - return null; - } - if (requireKnownStations - && ship.KnownStationIds.Count > 0 - && !ship.KnownStationIds.Contains(destination.Id) - && (homeStation is null || !string.Equals(destination.Id, homeStation.Id, StringComparison.Ordinal))) - { - return null; - } - if (string.Equals(behaviorKind, "find-build-tasks", StringComparison.Ordinal) && destinationSite is null) - { - return null; - } - if (!string.Equals(behaviorKind, "find-build-tasks", StringComparison.Ordinal) && destinationSite is not null) - { - return null; - } - - var source = stationsById.Values - .Where(station => - { - if (station.Id == destination.Id || GetInventoryAmount(station.Inventory, order.ItemId) <= GetStationReserveFloor(world, station, order.ItemId) + 1f) - { - return false; - } - - if (!TryCheckSystemAllowed(world, policy, ship.FactionId, station.SystemId, "trade", out var sourceDeniedReason)) - { - deniedReason ??= sourceDeniedReason; - return false; - } - - if (!IsWithinSystemRange(world, originSystemId, station.SystemId, rangeBudget)) - { - return false; - } - - return !requireKnownStations - || ship.KnownStationIds.Count == 0 - || ship.KnownStationIds.Contains(station.Id) - || (homeStation is not null && string.Equals(station.Id, homeStation.Id, StringComparison.Ordinal)); - }) - .OrderByDescending(station => GetInventoryAmount(station.Inventory, order.ItemId) - GetStationReserveFloor(world, station, order.ItemId)) - .ThenByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0) - .ThenBy(station => station.Id, StringComparer.Ordinal) - .FirstOrDefault(); - if (source is null) - { - return null; - } - - var shortageBias = string.Equals(behaviorKind, "fill-shortages", StringComparison.Ordinal) - ? GetFactionDemandScore(world, ship.FactionId, order.ItemId) * 35f - : 0f; - var buildBias = destinationSite is null ? 0f : 65f; - var revisitBias = string.Equals(behaviorKind, "revisit-known-stations", StringComparison.Ordinal) && ship.KnownStationIds.Contains(source.Id) && ship.KnownStationIds.Contains(destination.Id) - ? 28f - : 0f; - var regionalNeedBias = GetRegionalCommodityPressure(world, ship.FactionId, destination.SystemId, order.ItemId) * 18f; - var systemRangePenalty = (GetSystemDistanceTier(world, originSystemId, source.SystemId) + GetSystemDistanceTier(world, originSystemId, destination.SystemId)) * 16f; - var riskPenalty = - (GeopoliticalSimulationService.GetSystemRouteRisk(world, source.SystemId, ship.FactionId) - + GeopoliticalSimulationService.GetSystemRouteRisk(world, destination.SystemId, ship.FactionId)) * 22f; - var distanceScore = source.Position.DistanceTo(ship.Position) + source.Position.DistanceTo(destination.Position); - var score = (order.Valuation * 50f) - + shortageBias - + buildBias - + revisitBias - + regionalNeedBias - + (effectiveTradeSkill * 12f) - - systemRangePenalty - - riskPenalty - - distanceScore; - var summary = destinationSite is null - ? $"{order.ItemId}: {source.Label} -> {destination.Label}" - : $"{order.ItemId}: {source.Label} -> build support {destination.Label}"; - return new TradeRoutePlan(source, destination, order.ItemId, score, summary); - }) - .Where(route => route is not null) - .Cast() - .OrderByDescending(route => route.Score) - .ThenBy(route => route.ItemId, StringComparer.Ordinal) - .ThenBy(route => route.SourceStation.Id, StringComparer.Ordinal) - .FirstOrDefault(); - if (route is null && deniedReason is not null) - { - ship.LastAccessFailureReason = deniedReason; - } - - return route; - } - - private static FleetSupplyPlan? SelectFleetSupplyPlan(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation) - { - var assignment = ResolveAssignment(world, ship); - var targetCandidates = world.Ships - .Where(candidate => - candidate.Id != ship.Id && - candidate.FactionId == ship.FactionId && - candidate.Definition.CargoCapacity > 0.01f && - (assignment?.TargetEntityId is null || string.Equals(candidate.Id, assignment.TargetEntityId, StringComparison.Ordinal))) - .OrderByDescending(candidate => candidate.Definition.Kind == "military" ? 1 : 0) - .ThenBy(candidate => candidate.Id, StringComparer.Ordinal) - .ToList(); - if (targetCandidates.Count == 0) - { - return null; - } - - var sourceStations = world.Stations - .Where(station => station.FactionId == ship.FactionId) - .OrderByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0) - .ThenBy(station => station.Id, StringComparer.Ordinal) - .ToList(); - foreach (var target in targetCandidates) - { - var itemId = assignment?.ItemId - ?? sourceStations - .SelectMany(station => station.Inventory) - .Where(entry => entry.Value > 2f) - .OrderByDescending(entry => entry.Value) - .ThenBy(entry => entry.Key, StringComparer.Ordinal) - .Select(entry => entry.Key) - .FirstOrDefault(); - if (itemId is null) - { - continue; - } - - var source = sourceStations.FirstOrDefault(station => GetInventoryAmount(station.Inventory, itemId) > 2f); - if (source is null) - { - continue; - } - - var amount = MathF.Min(MathF.Max(10f, ship.Definition.CargoCapacity * 0.5f), GetInventoryAmount(source.Inventory, itemId)); - return new FleetSupplyPlan(source, target, itemId, amount, MathF.Max(16f, ship.DefaultBehavior.Radius), $"Supply {target.Definition.Label} with {itemId}"); - } - - return null; - } - - private static StationRuntime? SelectKnownStationVisit(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation) - { - var candidateIds = ship.KnownStationIds.Count == 0 && homeStation is not null - ? [homeStation.Id] - : ship.KnownStationIds.OrderBy(id => id, StringComparer.Ordinal).ToArray(); - return candidateIds - .Select(id => ResolveStation(world, id)) - .Where(station => station is not null && station.FactionId == ship.FactionId) - .Cast() - .OrderByDescending(station => homeStation is not null && station.Id == homeStation.Id ? 1 : 0) - .ThenBy(station => station.SystemId == ship.SystemId ? 0 : 1) - .ThenBy(station => station.Position.DistanceTo(ship.Position)) - .FirstOrDefault(); - } - - private static StationRuntime SelectBestDeliveryStation(SimulationWorld world, ShipRuntime ship, string itemId, StationRuntime homeStation, string behaviorKind) - { - if (!string.Equals(behaviorKind, "expert-auto-mine", StringComparison.Ordinal)) - { - return homeStation; - } - - return world.Stations - .Where(station => station.FactionId == ship.FactionId) - .OrderByDescending(station => GetFactionDemandScore(world, ship.FactionId, itemId) + GetRegionalCommodityPressure(world, ship.FactionId, station.SystemId, itemId) + (station.Id == homeStation.Id ? 5f : 0f)) - .ThenBy(station => station.SystemId == homeStation.SystemId ? 0 : 1) - .ThenBy(station => station.Id, StringComparer.Ordinal) - .FirstOrDefault() - ?? homeStation; - } - - private static float GetFactionDemandScore(SimulationWorld world, string factionId, string itemId) - { - var signal = CommanderPlanningService.FindFactionEconomicAssessment(world, factionId)? - .CommoditySignals - .FirstOrDefault(candidate => candidate.ItemId == itemId); - var regionalBottleneckScore = world.Geopolitics?.EconomyRegions.Bottlenecks - .Where(bottleneck => string.Equals(bottleneck.ItemId, itemId, StringComparison.Ordinal)) - .Join( - world.Geopolitics.EconomyRegions.Regions.Where(region => string.Equals(region.FactionId, factionId, StringComparison.Ordinal)), - bottleneck => bottleneck.RegionId, - region => region.Id, - (bottleneck, _) => bottleneck.Severity) - .DefaultIfEmpty() - .Max() ?? 0f; - if (signal is null) - { - return regionalBottleneckScore * 8f; - } - - return MathF.Max(0f, signal.BuyBacklog + signal.ReservedForConstruction + MathF.Max(0f, -signal.ProjectedNetRatePerSecond * 50f) + (regionalBottleneckScore * 8f)); - } - - private static float GetRegionalCommodityPressure(SimulationWorld world, string factionId, string systemId, string itemId) - { - var region = GeopoliticalSimulationService.GetPrimaryEconomicRegion(world, factionId, systemId); - if (region is null) - { - return 0f; - } - - var bottleneck = world.Geopolitics?.EconomyRegions.Bottlenecks - .FirstOrDefault(candidate => string.Equals(candidate.RegionId, region.Id, StringComparison.Ordinal) - && string.Equals(candidate.ItemId, itemId, StringComparison.Ordinal)); - var assessment = world.Geopolitics?.EconomyRegions.EconomicAssessments - .FirstOrDefault(candidate => string.Equals(candidate.RegionId, region.Id, StringComparison.Ordinal)); - return (bottleneck?.Severity ?? 0f) + ((assessment?.ConstructionPressure ?? 0f) * 2f); - } - - private static ThreatTargetCandidate? SelectThreatTarget( - SimulationWorld world, - ShipRuntime ship, - string targetSystemId, - Vector3 anchorPosition, - float radius, - string? excludeEntityId = null) - { - var policy = ResolvePolicy(world, ship.PolicySetId); - return world.Ships - .Where(candidate => - candidate.Id != excludeEntityId && - candidate.Health > 0f && - candidate.FactionId != ship.FactionId && - string.Equals(candidate.SystemId, targetSystemId, StringComparison.Ordinal) && - candidate.Position.DistanceTo(anchorPosition) <= radius * 1.75f) - .Select(candidate => new ThreatTargetCandidate( - candidate.Id, - candidate.SystemId, - candidate.Position, - 100f - + (candidate.Definition.Kind == "military" ? 30f : 0f) - - candidate.Position.DistanceTo(anchorPosition) - - candidate.Position.DistanceTo(ship.Position) - + (string.Equals(policy?.CombatEngagementPolicy, "aggressive", StringComparison.OrdinalIgnoreCase) ? 12f : 0f))) - .Concat(world.Stations - .Where(candidate => - candidate.Id != excludeEntityId && - candidate.FactionId != ship.FactionId && - string.Equals(candidate.SystemId, targetSystemId, StringComparison.Ordinal) && - candidate.Position.DistanceTo(anchorPosition) <= radius * 2f) - .Select(candidate => new ThreatTargetCandidate(candidate.Id, candidate.SystemId, candidate.Position, 45f - candidate.Position.DistanceTo(anchorPosition) * 0.2f))) - .OrderByDescending(candidate => candidate.Score) - .ThenBy(candidate => candidate.EntityId, StringComparer.Ordinal) - .FirstOrDefault(); - } - - private static PoliceContactCandidate? SelectPoliceContact(SimulationWorld world, ShipRuntime ship, string systemId, Vector3 anchorPosition, float radius) - { - var policy = ResolvePolicy(world, ship.PolicySetId); - return world.Ships - .Where(candidate => - candidate.Id != ship.Id && - candidate.Health > 0f && - candidate.FactionId != ship.FactionId && - string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal) && - candidate.Position.DistanceTo(anchorPosition) <= radius * 1.5f) - .Select(candidate => - { - var engage = candidate.Definition.Kind == "military" - || string.Equals(policy?.CombatEngagementPolicy, "aggressive", StringComparison.OrdinalIgnoreCase); - var score = (engage ? 80f : 40f) - - candidate.Position.DistanceTo(anchorPosition) - - candidate.Position.DistanceTo(ship.Position) - + (candidate.Definition.Kind == "transport" ? 8f : 0f); - return new PoliceContactCandidate(candidate.Id, candidate.SystemId, candidate.Position, engage, score); - }) - .OrderByDescending(candidate => candidate.Score) - .ThenBy(candidate => candidate.EntityId, StringComparer.Ordinal) - .FirstOrDefault(); - } - - private static SalvageOpportunity? SelectSalvageOpportunity(SimulationWorld world, ShipRuntime ship, StationRuntime? homeStation) - { - if (homeStation is null) - { - return null; - } - - var rangeBudget = ResolveBehaviorSystemRange(world, ship, "auto-salvage", ship.DefaultBehavior.MaxSystemRange > 0 ? ship.DefaultBehavior.MaxSystemRange : 1); - return world.Wrecks - .Where(wreck => - wreck.RemainingAmount > 0.01f && - IsWithinSystemRange(world, homeStation.SystemId, wreck.SystemId, rangeBudget)) - .Select(wreck => new SalvageOpportunity( - wreck, - (wreck.RemainingAmount * 3f) - wreck.Position.DistanceTo(ship.Position) - (GetSystemDistanceTier(world, homeStation.SystemId, wreck.SystemId) * 25f), - $"Salvage {wreck.ItemId} from {wreck.SourceEntityId}")) - .OrderByDescending(candidate => candidate.Score) - .ThenBy(candidate => candidate.Wreck.Id, StringComparer.Ordinal) - .FirstOrDefault(); - } - - private static (string SystemId, Vector3 Position)? ResolveObjectTarget(SimulationWorld world, string? entityId) - { - if (entityId is null) - { - return null; - } - - if (world.Ships.FirstOrDefault(candidate => candidate.Id == entityId) is { } ship) - { - return (ship.SystemId, ship.Position); - } - - if (ResolveStation(world, entityId) is { } station) - { - return (station.SystemId, station.Position); - } - - if (world.Celestials.FirstOrDefault(candidate => candidate.Id == entityId) is { } celestial) - { - return (celestial.SystemId, celestial.Position); - } - - if (world.ConstructionSites.FirstOrDefault(candidate => candidate.Id == entityId) is { } site) - { - var position = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? Vector3.Zero; - return (site.SystemId, position); - } - - if (world.Wrecks.FirstOrDefault(candidate => candidate.Id == entityId) is { } wreck) - { - return (wreck.SystemId, wreck.Position); - } - - return null; - } - - private static Vector3 GetFormationPosition(Vector3 anchorPosition, string seed, float radius) - { - var hash = Math.Abs(seed.Aggregate(17, (acc, c) => (acc * 31) + c)); - var angle = (hash % 360) * (MathF.PI / 180f); - return new Vector3( - anchorPosition.X + (MathF.Cos(angle) * radius), - anchorPosition.Y, - anchorPosition.Z + (MathF.Sin(angle) * radius)); - } - - private static TradeRoutePlan? ResolveTradeRoute(SimulationWorld world, string itemId, string sourceStationId, string destinationStationId) - { - var source = ResolveStation(world, sourceStationId); - var destination = ResolveStation(world, destinationStationId); - return source is null || destination is null ? null : new TradeRoutePlan(source, destination, itemId, 0f, $"{itemId}: {source.Label} -> {destination.Label}"); - } - - private static StationRuntime? ResolveStation(SimulationWorld world, string? stationId) => - stationId is null ? null : world.Stations.FirstOrDefault(candidate => candidate.Id == stationId); - - private static ResourceNodeRuntime? ResolveNode(SimulationWorld world, string? nodeId) => - nodeId is null ? null : world.Nodes.FirstOrDefault(candidate => candidate.Id == nodeId); - - private static PolicySetRuntime? ResolvePolicy(SimulationWorld world, string? policySetId) => - policySetId is null ? null : world.Policies.FirstOrDefault(policy => policy.Id == policySetId); - - private static bool IsSystemAllowed( - SimulationWorld world, - PolicySetRuntime? policy, - string factionId, - string systemId, - string accessKind) => - TryCheckSystemAllowed(world, policy, factionId, systemId, accessKind, out _); - - private static bool TryCheckSystemAllowed( - SimulationWorld world, - PolicySetRuntime? policy, - string factionId, - string systemId, - string accessKind, - out string? denialReason) - { - denialReason = null; - if (policy?.BlacklistedSystemIds.Contains(systemId) == true) - { - denialReason = $"blacklisted:{systemId}"; - return false; - } - - var controlState = GeopoliticalSimulationService.GetSystemControlState(world, systemId); - var authorityFactionId = controlState?.ControllerFactionId ?? controlState?.PrimaryClaimantFactionId; - if (authorityFactionId is null || string.Equals(authorityFactionId, factionId, StringComparison.Ordinal)) - { - return true; - } - - var hasAccess = string.Equals(accessKind, "trade", StringComparison.Ordinal) - ? GeopoliticalSimulationService.HasTradeAccess(world, factionId, authorityFactionId) - : GeopoliticalSimulationService.HasMilitaryAccess(world, factionId, authorityFactionId); - if (!hasAccess) - { - denialReason = $"{accessKind}-access-denied:{authorityFactionId}"; - return false; - } - - if (policy?.AvoidHostileSystems != true) - { - return true; - } - - if (GeopoliticalSimulationService.HasHostileRelation(world, factionId, authorityFactionId)) - { - denialReason = $"hostile-authority:{authorityFactionId}"; - return false; - } - - var hostileInfluencer = controlState?.InfluencingFactionIds.FirstOrDefault(candidate => - !string.Equals(candidate, factionId, StringComparison.Ordinal) - && GeopoliticalSimulationService.HasHostileRelation(world, factionId, candidate)); - if (hostileInfluencer is not null) - { - denialReason = $"hostile-influence:{hostileInfluencer}"; - return false; - } - - return true; - } - - private static CommanderAssignmentRuntime? ResolveAssignment(SimulationWorld world, ShipRuntime ship) => - ship.CommanderId is null - ? null - : world.Commanders.FirstOrDefault(candidate => candidate.Id == ship.CommanderId)?.Assignment; - - private static ShipOrderRuntime? GetTopOrder(ShipRuntime ship) => - ship.OrderQueue - .Where(order => order.Status is OrderStatus.Queued or OrderStatus.Active) - .OrderByDescending(order => order.Priority) - .ThenBy(order => order.CreatedAtUtc) - .FirstOrDefault(); - - private static ShipPlanStepRuntime? GetCurrentStep(ShipPlanRuntime? plan) => - plan is null || plan.CurrentStepIndex >= plan.Steps.Count ? null : plan.Steps[plan.CurrentStepIndex]; - - private static StationRuntime? ResolveSupportStation(SimulationWorld world, ShipRuntime ship, ConstructionSiteRuntime site) - { - return ResolveStation(world, ResolveAssignment(world, ship)?.HomeStationId ?? ship.DefaultBehavior.HomeStationId) - ?? world.Stations - .Where(station => station.FactionId == ship.FactionId) - .OrderByDescending(station => station.SystemId == site.SystemId ? 1 : 0) - .ThenBy(station => station.Id, StringComparer.Ordinal) - .FirstOrDefault(); - } - - private static Vector3 ResolveSupportPosition(ShipRuntime ship, StationRuntime station, ConstructionSiteRuntime? site, SimulationWorld world) - { - if (ship.DockedStationId is not null) - { - return GetShipDockedPosition(ship, station); - } - - if (site?.StationId is null && site is not null) - { - var anchorPosition = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId)?.Position ?? station.Position; - return GetResourceHoldPosition(anchorPosition, ship.Id, 78f); - } - - return GetConstructionHoldPosition(station, ship.Id); - } - - private static bool IsShipWithinSupportRange(ShipRuntime ship, Vector3 supportPosition, float threshold) => - ship.Position.DistanceTo(supportPosition) <= MathF.Max(threshold, 6f); - - private static void TrackHistory(ShipRuntime ship) - { - var plan = ship.ActivePlan; - var step = GetCurrentStep(plan); - var subTask = step is null || step.CurrentSubTaskIndex >= step.SubTasks.Count ? null : step.SubTasks[step.CurrentSubTaskIndex]; - var signature = $"{ship.State.ToContractValue()}|{plan?.Kind ?? "none"}|{step?.Kind ?? "none"}|{subTask?.Kind ?? "none"}|{GetShipCargoAmount(ship):0.0}"; - if (ship.LastSignature == signature) - { - return; - } - - ship.LastSignature = signature; - ship.History.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} state={ship.State.ToContractValue()} plan={plan?.Kind ?? "none"} step={step?.Kind ?? "none"} subTask={subTask?.Kind ?? "none"} cargo={GetShipCargoAmount(ship):0.#}"); - if (ship.History.Count > 24) - { - ship.History.RemoveAt(0); - } - } - - private static void EmitStateEvents(ShipRuntime ship, ShipState previousState, string? previousPlanId, string? previousStepId, ICollection events) - { - var currentPlanId = ship.ActivePlan?.Id; - var currentStepId = GetCurrentStep(ship.ActivePlan)?.Id; - var occurredAtUtc = DateTimeOffset.UtcNow; - if (previousState != ship.State) - { - events.Add(new SimulationEventRecord("ship", ship.Id, "state-changed", $"{ship.Definition.Label} state {previousState.ToContractValue()} -> {ship.State.ToContractValue()}.", occurredAtUtc)); - } - - if (!string.Equals(previousPlanId, currentPlanId, StringComparison.Ordinal)) - { - events.Add(new SimulationEventRecord("ship", ship.Id, "plan-changed", $"{ship.Definition.Label} switched active plan.", occurredAtUtc)); - } - - if (!string.Equals(previousStepId, currentStepId, StringComparison.Ordinal)) - { - events.Add(new SimulationEventRecord("ship", ship.Id, "step-changed", $"{ship.Definition.Label} advanced plan step.", occurredAtUtc)); - } - } - - private static void CompleteStationFoundation(SimulationWorld world, StationRuntime supportStation, ConstructionSiteRuntime site) - { - var anchor = world.Celestials.FirstOrDefault(candidate => candidate.Id == site.CelestialId); - if (anchor is null || site.BlueprintId is null) - { - site.State = ConstructionSiteStateKinds.Destroyed; - return; - } - - var station = new StationRuntime - { - Id = $"station-{world.Stations.Count + 1}", - SystemId = site.SystemId, - Label = BuildFoundedStationLabel(site.TargetDefinitionId), - Category = "station", - Objective = DetermineFoundationObjective(site.TargetDefinitionId), - Color = world.Factions.FirstOrDefault(candidate => candidate.Id == site.FactionId)?.Color ?? supportStation.Color, - Position = anchor.Position, - FactionId = site.FactionId, - CelestialId = site.CelestialId, - Health = 600f, - MaxHealth = 600f, - }; - - foreach (var moduleId in GetFoundationModules(world, site.BlueprintId)) - { - AddStationModule(world, station, moduleId); - } - - world.Stations.Add(station); - StationLifecycleService.EnsureStationCommander(world, station); - anchor.OccupyingStructureId = station.Id; - site.StationId = station.Id; - PrepareNextConstructionSiteStep(world, station, site); - } - - private static IReadOnlyList GetFoundationModules(SimulationWorld world, string primaryModuleId) - { - var modules = new List { "module_arg_dock_m_01_lowtech" }; - foreach (var itemId in world.ProductionGraph.OutputsByModuleId.GetValueOrDefault(primaryModuleId, [])) - { - if (world.ItemDefinitions.TryGetValue(itemId, out var itemDefinition)) - { - var storageModule = GetStorageRequirement(world.ModuleDefinitions, itemDefinition.CargoKind); - if (storageModule is not null && !modules.Contains(storageModule, StringComparer.Ordinal)) - { - modules.Add(storageModule); - } - } - } - - if (!modules.Contains("module_arg_stor_container_m_01", StringComparer.Ordinal)) - { - modules.Add("module_arg_stor_container_m_01"); - } - - if (!string.Equals(primaryModuleId, "module_gen_prod_energycells_01", StringComparison.Ordinal)) - { - modules.Add("module_gen_prod_energycells_01"); - } - - modules.Add(primaryModuleId); - return modules.Distinct(StringComparer.Ordinal).ToList(); - } - - private static string DetermineFoundationObjective(string commodityId) => - commodityId switch - { - "energycells" => "power", - "water" => "water", - "refinedmetals" => "refinery", - "hullparts" => "hullparts", - "claytronics" => "claytronics", - "shipyard" => "shipyard", - _ => "general", - }; - - private static string BuildFoundedStationLabel(string commodityId) => - $"{char.ToUpperInvariant(commodityId[0])}{commodityId[1..]} Foundry"; - - private enum SubTaskOutcome - { - Active, - Completed, - Failed, - } - - private sealed record TradeRoutePlan( - StationRuntime SourceStation, - StationRuntime DestinationStation, - string ItemId, - float Score, - string Summary); - - private sealed record MiningOpportunity( - ResourceNodeRuntime Node, - StationRuntime DropOffStation, - float Score, - string Summary); - - private sealed record FleetSupplyPlan( - StationRuntime SourceStation, - ShipRuntime TargetShip, - string ItemId, - float Amount, - float Radius, - string Summary); - - private sealed record ThreatTargetCandidate( - string EntityId, - string SystemId, - Vector3 Position, - float Score); - - private sealed record PoliceContactCandidate( - string EntityId, - string SystemId, - Vector3 Position, - bool Engage, - float Score); - - private sealed record SalvageOpportunity( - WreckRuntime Wreck, - float Score, - string Summary); -} diff --git a/apps/backend/Ships/Simulation/ShipBootstrapPolicy.cs b/apps/backend/Ships/Simulation/ShipBootstrapPolicy.cs deleted file mode 100644 index 523fb84..0000000 --- a/apps/backend/Ships/Simulation/ShipBootstrapPolicy.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace SpaceGame.Api.Ships.Simulation; - -internal static class ShipBootstrapPolicy -{ - internal static ShipSkillProfileRuntime CreateSkills(ShipDefinition definition) - { - return definition.Kind switch - { - "transport" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 4, Mining = 1, Combat = 1, Construction = 1 }, - "construction" => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 1, Combat = 1, Construction = 4 }, - "military" => new ShipSkillProfileRuntime { Navigation = 4, Trade = 1, Mining = 1, Combat = 4, Construction = 1 }, - _ when SpaceGame.Api.Universe.Scenario.LoaderSupport.HasCapabilities(definition, "mining") => new ShipSkillProfileRuntime { Navigation = 3, Trade = 1, Mining = 4, Combat = 1, Construction = 1 }, - _ => new ShipSkillProfileRuntime { Navigation = 3, Trade = 2, Mining = 1, Combat = 1, Construction = 1 }, - }; - } -} diff --git a/apps/backend/Simulation/Core/SimulationEngine.cs b/apps/backend/Simulation/Core/SimulationEngine.cs index 3692ba6..aa0cd16 100644 --- a/apps/backend/Simulation/Core/SimulationEngine.cs +++ b/apps/backend/Simulation/Core/SimulationEngine.cs @@ -3,6 +3,7 @@ namespace SpaceGame.Api.Simulation.Core; internal sealed class SimulationEngine { private readonly IBalanceService _balance; + private readonly IPlayerStateStore _playerStateStore; private readonly OrbitalSimulationOptions _orbitalSimulation; private readonly OrbitalStateUpdater _orbitalStateUpdater; private readonly InfrastructureSimulationService _infrastructureSimulation; @@ -14,9 +15,10 @@ internal sealed class SimulationEngine private readonly ShipAiService _shipAi; private readonly SimulationProjectionService _projection; - internal SimulationEngine(OrbitalSimulationOptions orbitalSimulation, IBalanceService balance) + internal SimulationEngine(OrbitalSimulationOptions orbitalSimulation, IBalanceService balance, IPlayerStateStore playerStateStore) { _balance = balance; + _playerStateStore = playerStateStore; _orbitalSimulation = orbitalSimulation; _orbitalStateUpdater = new OrbitalStateUpdater(_orbitalSimulation); _infrastructureSimulation = new InfrastructureSimulationService(); @@ -42,8 +44,8 @@ internal sealed class SimulationEngine _infrastructureSimulation.UpdateClaims(world, events); _infrastructureSimulation.UpdateConstructionSites(world, events); _geopolitics.Update(world, simulationDeltaSeconds, events); - _commanderPlanning.UpdateCommanders(world, simulationDeltaSeconds, events); - _playerFaction.Update(world, simulationDeltaSeconds, events); + _commanderPlanning.UpdateCommanders(world, _playerStateStore, simulationDeltaSeconds, events); + _playerFaction.Update(world, _playerStateStore, simulationDeltaSeconds, events); _stationLifecycle.UpdateStations(world, simulationDeltaSeconds, events); foreach (var ship in world.Ships.ToList()) @@ -76,7 +78,7 @@ internal sealed class SimulationEngine { foreach (var ship in world.Ships.Where(candidate => candidate.Health <= 0f).ToList()) { - CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.CargoCapacity + (ship.Definition.MaxHealth * 0.08f)); + CreateWreck(world, "ship", ship.Id, ship.SystemId, ship.Position, ship.Definition.GetTotalCargoCapacity() + (ship.Definition.Hull * 0.08f)); world.Ships.Remove(ship); if (ship.DockedStationId is not null && world.Stations.FirstOrDefault(station => station.Id == ship.DockedStationId) is { } dockedStation) { @@ -94,7 +96,7 @@ internal sealed class SimulationEngine commander.IsAlive = false; } - events.Add(new SimulationEventRecord("ship", ship.Id, "destroyed", $"{ship.Definition.Label} was destroyed.", DateTimeOffset.UtcNow)); + events.Add(new SimulationEventRecord("ship", ship.Id, "destroyed", $"{ship.Definition.Name} was destroyed.", DateTimeOffset.UtcNow)); } foreach (var station in world.Stations.Where(candidate => candidate.Health <= 0f).ToList()) diff --git a/apps/backend/Simulation/Core/SimulationProjectionService.cs b/apps/backend/Simulation/Core/SimulationProjectionService.cs index eb8a174..6feb72f 100644 --- a/apps/backend/Simulation/Core/SimulationProjectionService.cs +++ b/apps/backend/Simulation/Core/SimulationProjectionService.cs @@ -32,7 +32,6 @@ internal sealed class SimulationProjectionService BuildPolicyDeltas(world), BuildShipDeltas(world), BuildFactionDeltas(world), - BuildPlayerFactionDelta(world), BuildGeopoliticsDelta(world)); public WorldSnapshot BuildSnapshot(SimulationWorld world, long sequence) @@ -177,9 +176,9 @@ internal sealed class SimulationProjectionService policy.BlacklistedSystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList())).ToList(), world.Ships.Select(ship => ToShipDelta(world, ship)).Select(ship => new ShipSnapshot( ship.Id, - ship.Label, - ship.Kind, - ship.Class, + ship.Name, + ship.Purpose, + ship.Type, ship.SystemId, ship.LocalPosition, ship.LocalVelocity, @@ -225,7 +224,6 @@ internal sealed class SimulationProjectionService faction.StrategicState, faction.DecisionLog, faction.Commanders)).ToList(), - ToPlayerFactionSnapshot(world.PlayerFaction), ToGeopoliticalStateSnapshot(world.Geopolitics)); } @@ -276,11 +274,6 @@ internal sealed class SimulationProjectionService faction.LastDeltaSignature = BuildFactionSignature(faction, FindFactionCommander(world, faction.Id)); } - if (world.PlayerFaction is not null) - { - world.PlayerFaction.LastDeltaSignature = BuildPlayerFactionSignature(world.PlayerFaction); - } - if (world.Geopolitics is not null) { world.Geopolitics.LastDeltaSignature = BuildGeopoliticalSignature(world.Geopolitics); @@ -450,23 +443,6 @@ internal sealed class SimulationProjectionService return deltas; } - private static PlayerFactionSnapshot? BuildPlayerFactionDelta(SimulationWorld world) - { - if (world.PlayerFaction is null) - { - return null; - } - - var signature = BuildPlayerFactionSignature(world.PlayerFaction); - if (signature == world.PlayerFaction.LastDeltaSignature) - { - return null; - } - - world.PlayerFaction.LastDeltaSignature = signature; - return ToPlayerFactionSnapshot(world.PlayerFaction); - } - private static GeopoliticalStateSnapshot? BuildGeopoliticsDelta(SimulationWorld world) { if (world.Geopolitics is null) @@ -544,11 +520,13 @@ internal sealed class SimulationProjectionService ship.TargetPosition.Z.ToString("0.###"), ship.State.ToContractValue(), string.Join(",", ship.OrderQueue - .OrderByDescending(order => order.Priority) + .OrderByDescending(GetOrderSourcePriority) + .ThenByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) - .Select(order => $"{order.Id}:{order.Kind}:{order.Status.ToContractValue()}:{order.Priority}:{order.TargetEntityId}:{order.TargetSystemId}:{order.ItemId}:{order.WaitSeconds:0.###}:{order.Radius:0.###}:{order.MaxSystemRange?.ToString(CultureInfo.InvariantCulture) ?? "none"}:{order.KnownStationsOnly}")), + .Select(order => $"{order.Id}:{order.Kind}:{order.SourceKind.ToContractValue()}:{order.SourceId}:{order.Status.ToContractValue()}:{order.Priority}:{order.TargetEntityId}:{order.TargetSystemId}:{order.ItemId}:{order.WaitSeconds:0.###}:{order.Radius:0.###}:{order.MaxSystemRange?.ToString(CultureInfo.InvariantCulture) ?? "none"}:{order.KnownStationsOnly}")), ship.DefaultBehavior.Kind, ship.DefaultBehavior.TargetEntityId ?? "none", + ship.DefaultBehavior.ItemId ?? "none", ship.DefaultBehavior.TargetPosition?.X.ToString("0.###") ?? "none", ship.DefaultBehavior.TargetPosition?.Y.ToString("0.###") ?? "none", ship.DefaultBehavior.TargetPosition?.Z.ToString("0.###") ?? "none", @@ -642,59 +620,6 @@ internal sealed class SimulationProjectionService return $"{faction.Credits:0.###}|{faction.PopulationTotal:0.###}|{faction.OreMined:0.###}|{faction.GoodsProduced:0.###}|{faction.ShipsBuilt}|{faction.ShipsLost}|{faction.DefaultPolicySetId}|{assignmentSig}|{strategicSig}|{doctrineSig}|{decisionSig}|{theaterSig}|{campaignSig}|{objectiveSig}|{reservationSig}|{productionSig}"; } - private static string BuildPlayerFactionSignature(PlayerFactionRuntime player) - { - var intentSig = $"{player.StrategicIntent.StrategicPosture}:{player.StrategicIntent.EconomicPosture}:{player.StrategicIntent.MilitaryPosture}:{player.StrategicIntent.LogisticsPosture}:{player.StrategicIntent.DesiredReserveRatio:0.###}"; - var registrySig = string.Join("|", - player.AssetRegistry.ShipIds.Count, - player.AssetRegistry.StationIds.Count, - player.AssetRegistry.CommanderIds.Count, - player.AssetRegistry.FleetIds.Count, - player.AssetRegistry.TaskForceIds.Count, - player.AssetRegistry.StationGroupIds.Count, - player.AssetRegistry.EconomicRegionIds.Count, - player.AssetRegistry.FrontIds.Count, - player.AssetRegistry.ReserveIds.Count); - var orgSig = string.Join("|", - player.Fleets.Count, - player.TaskForces.Count, - player.StationGroups.Count, - player.EconomicRegions.Count, - player.Fronts.Count, - player.Reserves.Count, - player.Policies.Count, - player.AutomationPolicies.Count, - player.ReinforcementPolicies.Count, - player.ProductionPrograms.Count, - player.Directives.Count, - player.Assignments.Count, - player.Alerts.Count); - var policySig = string.Join(";", - player.Policies.OrderBy(policy => policy.Id, StringComparer.Ordinal) - .Select(policy => $"{policy.Id}:{policy.ScopeKind}:{policy.ScopeId}:{policy.PolicySetId}:{policy.TradeAccessPolicy}:{policy.DockingAccessPolicy}:{policy.ConstructionAccessPolicy}:{policy.OperationalRangePolicy}:{policy.CombatEngagementPolicy}:{policy.AvoidHostileSystems}:{policy.FleeHullRatio:0.###}:{policy.UpdatedAtUtc.UtcTicks}")); - var automationSig = string.Join(";", - player.AutomationPolicies.OrderBy(policy => policy.Id, StringComparer.Ordinal) - .Select(policy => $"{policy.Id}:{policy.ScopeKind}:{policy.ScopeId}:{policy.Enabled}:{policy.BehaviorKind}:{policy.UseOrders}:{policy.StagingOrderKind}:{policy.MaxSystemRange}:{policy.KnownStationsOnly}:{policy.Radius:0.###}:{policy.WaitSeconds:0.###}:{policy.PreferredItemId}:{policy.UpdatedAtUtc.UtcTicks}")); - var directiveSig = string.Join(";", - player.Directives.OrderBy(directive => directive.Id, StringComparer.Ordinal) - .Select(directive => $"{directive.Id}:{directive.ScopeKind}:{directive.ScopeId}:{directive.Kind}:{directive.BehaviorKind}:{directive.UseOrders}:{directive.StagingOrderKind}:{directive.TargetEntityId}:{directive.TargetSystemId}:{directive.ItemId}:{directive.Priority}:{directive.UpdatedAtUtc.UtcTicks}")); - var assignmentSig = string.Join(";", - player.Assignments.OrderBy(assignment => assignment.Id, StringComparer.Ordinal) - .Select(assignment => $"{assignment.Id}:{assignment.AssetKind}:{assignment.AssetId}:{assignment.FleetId}:{assignment.TaskForceId}:{assignment.StationGroupId}:{assignment.EconomicRegionId}:{assignment.FrontId}:{assignment.ReserveId}:{assignment.DirectiveId}:{assignment.PolicyId}:{assignment.AutomationPolicyId}:{assignment.Role}:{assignment.Status}:{assignment.UpdatedAtUtc.UtcTicks}")); - var decisionSig = string.Join(",", player.DecisionLog.Select(entry => entry.Id)); - var orgDetailSig = string.Join(";", - player.Fleets.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"fleet:{entry.Id}:{entry.FrontId}:{entry.HomeSystemId}:{entry.HomeStationId}:{entry.CommanderId}:{entry.UpdatedAtUtc.UtcTicks}") - .Concat(player.TaskForces.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"task-force:{entry.Id}:{entry.FleetId}:{entry.FrontId}:{entry.CommanderId}:{entry.UpdatedAtUtc.UtcTicks}")) - .Concat(player.StationGroups.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"station-group:{entry.Id}:{entry.EconomicRegionId}:{entry.UpdatedAtUtc.UtcTicks}")) - .Concat(player.EconomicRegions.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"economic-region:{entry.Id}:{entry.SharedEconomicRegionId}:{entry.Role}:{entry.UpdatedAtUtc.UtcTicks}")) - .Concat(player.Fronts.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"front:{entry.Id}:{entry.SharedFrontLineId}:{entry.TargetFactionId}:{entry.Priority:0.###}:{entry.UpdatedAtUtc.UtcTicks}")) - .Concat(player.Reserves.OrderBy(entry => entry.Id, StringComparer.Ordinal).Select(entry => $"reserve:{entry.Id}:{entry.HomeSystemId}:{entry.UpdatedAtUtc.UtcTicks}"))); - var alertSig = string.Join(";", - player.Alerts.OrderBy(alert => alert.Id, StringComparer.Ordinal) - .Select(alert => $"{alert.Id}:{alert.Kind}:{alert.Severity}:{alert.AssetKind}:{alert.AssetId}:{alert.RelatedDirectiveId}:{alert.Status}:{alert.CreatedAtUtc.UtcTicks}")); - return $"{player.SovereignFactionId}|{player.Status}|{intentSig}|{registrySig}|{orgSig}|{policySig}|{automationSig}|{directiveSig}|{assignmentSig}|{decisionSig}|{orgDetailSig}|{alertSig}"; - } - private static string BuildGeopoliticalSignature(GeopoliticalStateRuntime state) { var diplomacySig = string.Join(";", @@ -882,9 +807,9 @@ internal sealed class SimulationProjectionService return new ShipDelta( ship.Id, - ship.Definition.Label, - ship.Definition.Kind, - ship.Definition.Class, + ship.Definition.Name, + ship.Definition.Purpose.ToDataValue(), + ship.Definition.Type.ToDataValue(), ship.SystemId, ToDto(ship.Position), ToDto(ship.Velocity), @@ -906,7 +831,7 @@ internal sealed class SimulationProjectionService ship.DockedStationId, ship.CommanderId, ship.PolicySetId, - ship.Definition.CargoCapacity, + ship.Definition.GetTotalCargoCapacity(), ToShipTravelSpeed(ship).Speed, ToShipTravelSpeed(ship).Unit, @@ -936,11 +861,14 @@ internal sealed class SimulationProjectionService private static IReadOnlyList ToShipOrderSnapshots(ShipRuntime ship) => ship.OrderQueue - .OrderByDescending(order => order.Priority) + .OrderByDescending(GetOrderSourcePriority) + .ThenByDescending(order => order.Priority) .ThenBy(order => order.CreatedAtUtc) .Select(order => new ShipOrderSnapshot( order.Id, order.Kind, + order.SourceKind.ToContractValue(), + order.SourceId, order.Status.ToContractValue(), order.Priority, order.InterruptCurrentPlan, @@ -962,6 +890,14 @@ internal sealed class SimulationProjectionService order.FailureReason)) .ToList(); + private static int GetOrderSourcePriority(ShipOrderRuntime order) => order.SourceKind switch + { + ShipOrderSourceKind.Player => 300, + ShipOrderSourceKind.Commander => 200, + ShipOrderSourceKind.Behavior => 100, + _ => 0, + }; + private static DefaultBehaviorSnapshot ToDefaultBehaviorSnapshot(DefaultBehaviorRuntime behavior) => new( behavior.Kind, @@ -969,7 +905,7 @@ internal sealed class SimulationProjectionService behavior.HomeStationId, behavior.AreaSystemId, behavior.TargetEntityId, - behavior.PreferredItemId, + behavior.ItemId, behavior.PreferredNodeId, behavior.PreferredConstructionSiteId, behavior.PreferredModuleId, @@ -1385,252 +1321,6 @@ internal sealed class SimulationProjectionService entry.OccurredAtUtc)) .ToList(); - private static PlayerFactionSnapshot? ToPlayerFactionSnapshot(PlayerFactionRuntime? player) - { - if (player is null) - { - return null; - } - - return new PlayerFactionSnapshot( - player.Id, - player.Label, - player.SovereignFactionId, - player.Status, - player.CreatedAtUtc, - player.UpdatedAtUtc, - new PlayerAssetRegistrySnapshot( - player.AssetRegistry.ShipIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.StationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.CommanderIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.ClaimIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.ConstructionSiteIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.PolicySetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.MarketOrderIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.FleetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.TaskForceIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.StationGroupIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.EconomicRegionIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.FrontIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - player.AssetRegistry.ReserveIds.OrderBy(id => id, StringComparer.Ordinal).ToList()), - new PlayerStrategicIntentSnapshot( - player.StrategicIntent.StrategicPosture, - player.StrategicIntent.EconomicPosture, - player.StrategicIntent.MilitaryPosture, - player.StrategicIntent.LogisticsPosture, - player.StrategicIntent.DesiredReserveRatio, - player.StrategicIntent.AllowDelegatedCombatAutomation, - player.StrategicIntent.AllowDelegatedEconomicAutomation, - player.StrategicIntent.Notes), - player.Fleets.Select(fleet => new PlayerFleetSnapshot( - fleet.Id, - fleet.Label, - fleet.Status, - fleet.Role, - fleet.CommanderId, - fleet.FrontId, - fleet.HomeSystemId, - fleet.HomeStationId, - fleet.PolicyId, - fleet.AutomationPolicyId, - fleet.ReinforcementPolicyId, - fleet.AssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - fleet.TaskForceIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - fleet.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - fleet.UpdatedAtUtc)).ToList(), - player.TaskForces.Select(taskForce => new PlayerTaskForceSnapshot( - taskForce.Id, - taskForce.Label, - taskForce.Status, - taskForce.Role, - taskForce.FleetId, - taskForce.CommanderId, - taskForce.FrontId, - taskForce.PolicyId, - taskForce.AutomationPolicyId, - taskForce.AssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - taskForce.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - taskForce.UpdatedAtUtc)).ToList(), - player.StationGroups.Select(group => new PlayerStationGroupSnapshot( - group.Id, - group.Label, - group.Status, - group.Role, - group.EconomicRegionId, - group.PolicyId, - group.AutomationPolicyId, - group.StationIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - group.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - group.FocusItemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - group.UpdatedAtUtc)).ToList(), - player.EconomicRegions.Select(region => new PlayerEconomicRegionSnapshot( - region.Id, - region.Label, - region.Status, - region.Role, - region.SharedEconomicRegionId, - region.PolicyId, - region.AutomationPolicyId, - region.SystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - region.StationGroupIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - region.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - region.UpdatedAtUtc)).ToList(), - player.Fronts.Select(front => new PlayerFrontSnapshot( - front.Id, - front.Label, - front.Status, - front.Priority, - front.Posture, - front.SharedFrontLineId, - front.TargetFactionId, - front.SystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - front.FleetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - front.ReserveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - front.DirectiveIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - front.UpdatedAtUtc)).ToList(), - player.Reserves.Select(reserve => new PlayerReserveGroupSnapshot( - reserve.Id, - reserve.Label, - reserve.Status, - reserve.ReserveKind, - reserve.HomeSystemId, - reserve.PolicyId, - reserve.AssetIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - reserve.FrontIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - reserve.UpdatedAtUtc)).ToList(), - player.Policies.Select(policy => new PlayerFactionPolicySnapshot( - policy.Id, - policy.Label, - policy.ScopeKind, - policy.ScopeId, - policy.PolicySetId, - policy.AllowDelegatedCombat, - policy.AllowDelegatedTrade, - policy.ReserveCreditsRatio, - policy.ReserveMilitaryRatio, - policy.TradeAccessPolicy, - policy.DockingAccessPolicy, - policy.ConstructionAccessPolicy, - policy.OperationalRangePolicy, - policy.CombatEngagementPolicy, - policy.AvoidHostileSystems, - policy.FleeHullRatio, - policy.BlacklistedSystemIds.OrderBy(id => id, StringComparer.Ordinal).ToList(), - policy.Notes, - policy.UpdatedAtUtc)).ToList(), - player.AutomationPolicies.Select(policy => new PlayerAutomationPolicySnapshot( - policy.Id, - policy.Label, - policy.ScopeKind, - policy.ScopeId, - policy.Enabled, - policy.BehaviorKind, - policy.UseOrders, - policy.StagingOrderKind, - policy.MaxSystemRange, - policy.KnownStationsOnly, - policy.Radius, - policy.WaitSeconds, - policy.PreferredItemId, - policy.Notes, - policy.RepeatOrders.Select(ToShipOrderTemplateSnapshot).ToList(), - policy.UpdatedAtUtc)).ToList(), - player.ReinforcementPolicies.Select(policy => new PlayerReinforcementPolicySnapshot( - policy.Id, - policy.Label, - policy.ScopeKind, - policy.ScopeId, - policy.ShipKind, - policy.DesiredAssetCount, - policy.MinimumReserveCount, - policy.AutoTransferReserves, - policy.AutoQueueProduction, - policy.SourceReserveId, - policy.TargetFrontId, - policy.Notes, - policy.UpdatedAtUtc)).ToList(), - player.ProductionPrograms.Select(program => new PlayerProductionProgramSnapshot( - program.Id, - program.Label, - program.Status, - program.Kind, - program.TargetShipKind, - program.TargetModuleId, - program.TargetItemId, - program.TargetCount, - program.CurrentCount, - program.StationGroupId, - program.ReinforcementPolicyId, - program.Notes, - program.UpdatedAtUtc)).ToList(), - player.Directives.Select(directive => new PlayerDirectiveSnapshot( - directive.Id, - directive.Label, - directive.Status, - directive.Kind, - directive.ScopeKind, - directive.ScopeId, - directive.TargetEntityId, - directive.TargetSystemId, - directive.TargetPosition is null ? null : ToDto(directive.TargetPosition.Value), - directive.HomeSystemId, - directive.HomeStationId, - directive.SourceStationId, - directive.DestinationStationId, - directive.BehaviorKind, - directive.UseOrders, - directive.StagingOrderKind, - directive.ItemId, - directive.PreferredNodeId, - directive.PreferredConstructionSiteId, - directive.PreferredModuleId, - directive.Priority, - directive.Radius, - directive.WaitSeconds, - directive.MaxSystemRange, - directive.KnownStationsOnly, - directive.PatrolPoints.Select(ToDto).ToList(), - directive.RepeatOrders.Select(ToShipOrderTemplateSnapshot).ToList(), - directive.PolicyId, - directive.AutomationPolicyId, - directive.Notes, - directive.CreatedAtUtc, - directive.UpdatedAtUtc)).ToList(), - player.Assignments.Select(assignment => new PlayerAssignmentSnapshot( - assignment.Id, - assignment.AssetKind, - assignment.AssetId, - assignment.FleetId, - assignment.TaskForceId, - assignment.StationGroupId, - assignment.EconomicRegionId, - assignment.FrontId, - assignment.ReserveId, - assignment.DirectiveId, - assignment.PolicyId, - assignment.AutomationPolicyId, - assignment.Role, - assignment.Status, - assignment.UpdatedAtUtc)).ToList(), - player.DecisionLog.Select(entry => new PlayerDecisionLogEntrySnapshot( - entry.Id, - entry.Kind, - entry.Summary, - entry.RelatedEntityKind, - entry.RelatedEntityId, - entry.OccurredAtUtc)).ToList(), - player.Alerts.Select(alert => new PlayerAlertSnapshot( - alert.Id, - alert.Kind, - alert.Severity, - alert.Summary, - alert.AssetKind, - alert.AssetId, - alert.RelatedDirectiveId, - alert.Status, - alert.CreatedAtUtc)).ToList()); - } - private static GeopoliticalStateSnapshot? ToGeopoliticalStateSnapshot(GeopoliticalStateRuntime? state) { if (state is null) diff --git a/apps/backend/SpaceGame.Api.csproj b/apps/backend/SpaceGame.Api.csproj index 134b0f4..bab7aba 100644 --- a/apps/backend/SpaceGame.Api.csproj +++ b/apps/backend/SpaceGame.Api.csproj @@ -9,6 +9,8 @@ + + diff --git a/apps/backend/Stations/Simulation/StationLifecycleService.cs b/apps/backend/Stations/Simulation/StationLifecycleService.cs index b4ca6af..599c3a9 100644 --- a/apps/backend/Stations/Simulation/StationLifecycleService.cs +++ b/apps/backend/Stations/Simulation/StationLifecycleService.cs @@ -1,5 +1,6 @@ using SpaceGame.Api.Shared.Runtime; -using SpaceGame.Api.Ships.Simulation; +using SpaceGame.Api.Ships.AI; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Stations.Simulation; @@ -81,7 +82,7 @@ internal sealed class StationLifecycleService SpatialState = CreateSpawnedShipSpatialState(station, spawnPosition), DefaultBehavior = CreateSpawnedShipBehavior(definition, station), Skills = ShipBootstrapPolicy.CreateSkills(definition), - Health = definition.MaxHealth, + Health = definition.Hull, }; world.Ships.Add(ship); @@ -91,7 +92,7 @@ internal sealed class StationLifecycleService faction.ShipsBuilt += 1; } - events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Label} launched {definition.Label}.", DateTimeOffset.UtcNow)); + events.Add(new SimulationEventRecord("station", station.Id, "ship-built", $"{station.Label} launched {definition.Name}.", DateTimeOffset.UtcNow)); return 1f; } @@ -107,21 +108,22 @@ internal sealed class StationLifecycleService private static DefaultBehaviorRuntime CreateSpawnedShipBehavior(ShipDefinition definition, StationRuntime station) { - if (!string.Equals(definition.Kind, "military", StringComparison.Ordinal)) + if (!IsMilitaryShip(definition)) { return new DefaultBehaviorRuntime { - Kind = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? "advanced-auto-trade" : "idle", + Kind = IsTransportShip(definition) ? AdvancedAutoTrade : HoldPosition, HomeSystemId = station.SystemId, HomeStationId = station.Id, - MaxSystemRange = string.Equals(definition.Kind, "transport", StringComparison.Ordinal) ? 2 : 0, + AreaSystemId = station.SystemId, + MaxSystemRange = IsTransportShip(definition) ? 2 : 0, }; } var patrolRadius = station.Radius + 90f; return new DefaultBehaviorRuntime { - Kind = "patrol", + Kind = Patrol, HomeSystemId = station.SystemId, HomeStationId = station.Id, AreaSystemId = station.SystemId, diff --git a/apps/backend/Stations/Simulation/StationSimulationService.cs b/apps/backend/Stations/Simulation/StationSimulationService.cs index eb0f35f..9358608 100644 --- a/apps/backend/Stations/Simulation/StationSimulationService.cs +++ b/apps/backend/Stations/Simulation/StationSimulationService.cs @@ -1,4 +1,5 @@ using static SpaceGame.Api.Factions.AI.CommanderPlanningService; +using static SpaceGame.Api.Shared.Runtime.KnownShipTypes; using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; using SpaceGame.Api.Shared.Runtime; @@ -7,6 +8,10 @@ namespace SpaceGame.Api.Stations.Simulation; internal sealed class StationSimulationService { internal const int StrategicControlTargetSystems = 5; + private const string MilitaryShipCategory = "military"; + private const string ConstructionShipCategory = "construction"; + private const string TransportShipCategory = "transport"; + private const string MiningShipCategory = "mining"; internal void ReviewStationMarketOrders(SimulationWorld world, StationRuntime station) { @@ -63,7 +68,7 @@ internal sealed class StationSimulationService var superfluidCoolantReserve = role == "superfluidcoolant" ? 120f : 0f; var quantumTubesReserve = role == "quantumtubes" ? 120f : 0f; var shipPartsReserve = HasShipyardCapability(station) - && GetShipProductionPressure(world, station.FactionId, "military") > 0.2f + && GetShipProductionPressure(world, station.FactionId, MilitaryShipCategory) > 0.2f ? 90f : 0f; @@ -118,7 +123,7 @@ internal sealed class StationSimulationService var constructionClayReserve = GetConstructionDemandForItem(world, site, "claytronics"); var constructionRefinedReserve = GetConstructionDemandForItem(world, site, "refinedmetals"); var shipPartsReserve = HasShipyardCapability(station) - && GetShipProductionPressure(world, station.FactionId, "military") > 0.2f + && GetShipProductionPressure(world, station.FactionId, MilitaryShipCategory) > 0.2f ? 90f : 0f; @@ -255,7 +260,7 @@ internal sealed class StationSimulationService var priority = (float)recipe.Priority; var expansionPressure = GetFactionExpansionPressure(world, station.FactionId); - var fleetPressure = GetShipProductionPressure(world, station.FactionId, "military"); + var fleetPressure = GetShipProductionPressure(world, station.FactionId, MilitaryShipCategory); priority += GetStationRecipePriorityAdjustment(world, station, recipe, expansionPressure, fleetPressure); priority += GetStrategicRecipeBias(world, station, recipe); @@ -266,21 +271,34 @@ internal sealed class StationSimulationService { if (recipe.ShipOutputId is not null && world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition)) { - var shipPressure = GetShipProductionPressure(world, station.FactionId, shipDefinition.Kind); - return shipDefinition.Kind switch + var shipPressure = GetShipProductionPressure(world, station.FactionId, GetShipCategory(shipDefinition)); + if (IsMilitaryShip(shipDefinition)) { - "military" => recipe.Id switch + return recipe.Id switch { "frigate-construction" => 320f * shipPressure, "destroyer-construction" => 200f * shipPressure, "cruiser-construction" => 120f * shipPressure, _ => 160f * shipPressure, - }, - "construction" => 260f * shipPressure, - "mining" => 250f * shipPressure, - "transport" => 230f * shipPressure, - _ => 0f, - }; + }; + } + + if (IsConstructionShip(shipDefinition)) + { + return 260f * shipPressure; + } + + if (IsMiningShip(shipDefinition)) + { + return 250f * shipPressure; + } + + if (IsTransportShip(shipDefinition)) + { + return 230f * shipPressure; + } + + return 0f; } var outputItemIds = recipe.Outputs @@ -338,7 +356,7 @@ internal sealed class StationSimulationService if (string.Equals(assignment.Kind, "ship-production-focus", StringComparison.Ordinal) && recipe.ShipOutputId is not null && world.ShipDefinitions.TryGetValue(recipe.ShipOutputId, out var shipDefinition) - && string.Equals(shipDefinition.Kind, "military", StringComparison.Ordinal)) + && IsMilitaryShip(shipDefinition)) { return 260f; } @@ -383,7 +401,7 @@ internal sealed class StationSimulationService return false; } - if (GetShipProductionPressure(world, station.FactionId, shipDefinition.Kind) <= 0.05f) + if (GetShipProductionPressure(world, station.FactionId, GetShipCategory(shipDefinition)) <= 0.05f) { return false; } @@ -708,7 +726,7 @@ internal sealed class StationSimulationService .ToList(); } - private static float GetShipProductionPressure(SimulationWorld world, string factionId, string shipKind) + private static float GetShipProductionPressure(SimulationWorld world, string factionId, string? shipCategory) { var economic = FindFactionEconomicAssessment(world, factionId); var threat = FindFactionThreatAssessment(world, factionId); @@ -717,16 +735,16 @@ internal sealed class StationSimulationService return 0f; } - return shipKind switch + return shipCategory switch { - "military" => threat.EnemyFactionCount > 0 + MilitaryShipCategory => threat.EnemyFactionCount > 0 ? economic.MilitaryShipCount < Math.Max(4, economic.ControlledSystemCount * 2) ? 1f : 0.25f : 0.1f, - "construction" => economic.PrimaryExpansionSiteId is not null + ConstructionShipCategory => economic.PrimaryExpansionSiteId is not null ? economic.ConstructorShipCount < 1 ? 1f : 0.35f : economic.ConstructorShipCount < 1 ? 0.5f : 0f, - "transport" => economic.TransportShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.8f : 0.2f, - _ when shipKind == "mining" || shipKind == "miner" => economic.MinerShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.85f : 0.2f, + TransportShipCategory => economic.TransportShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.8f : 0.2f, + MiningShipCategory => economic.MinerShipCount < Math.Max(2, economic.ControlledSystemCount) ? 0.85f : 0.2f, _ => 0.15f, }; } diff --git a/apps/backend/Universe/Api/CreateFactionHandler.cs b/apps/backend/Universe/Api/CreateFactionHandler.cs new file mode 100644 index 0000000..32f9583 --- /dev/null +++ b/apps/backend/Universe/Api/CreateFactionHandler.cs @@ -0,0 +1,25 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Universe.Api; + +public sealed class CreateFactionHandler(WorldService worldService) : Endpoint +{ + public override void Configure() + { + Post("/api/gm/factions"); + Policies(AuthPolicyNames.GmAccess); + } + + public override async Task HandleAsync(CreateFactionCommandRequest request, CancellationToken cancellationToken) + { + try + { + await SendOkAsync(worldService.CreateFaction(request.FactionId), cancellationToken); + } + catch (InvalidOperationException ex) + { + AddError(ex.Message); + await SendErrorsAsync(cancellation: cancellationToken); + } + } +} diff --git a/apps/backend/Universe/Api/GetBalanceHandler.cs b/apps/backend/Universe/Api/GetBalanceHandler.cs index 0f3d674..890283c 100644 --- a/apps/backend/Universe/Api/GetBalanceHandler.cs +++ b/apps/backend/Universe/Api/GetBalanceHandler.cs @@ -8,7 +8,7 @@ public sealed class GetBalanceHandler(IBalanceService balanceService) : Endpoint public override void Configure() { Get("/api/balance"); - AllowAnonymous(); + Policies(AuthPolicyNames.GmAccess); } public override Task HandleAsync(CancellationToken cancellationToken) => diff --git a/apps/backend/Universe/Api/GetTelemetryHandler.cs b/apps/backend/Universe/Api/GetTelemetryHandler.cs index 5aebea8..0ebc525 100644 --- a/apps/backend/Universe/Api/GetTelemetryHandler.cs +++ b/apps/backend/Universe/Api/GetTelemetryHandler.cs @@ -9,7 +9,7 @@ public sealed class GetTelemetryHandler(TelemetryService telemetry, WorldService public override void Configure() { Get("/api/telemetry"); - AllowAnonymous(); + Policies(AuthPolicyNames.GmAccess); } public override Task HandleAsync(CancellationToken cancellationToken) diff --git a/apps/backend/Universe/Api/GetVersionHandler.cs b/apps/backend/Universe/Api/GetVersionHandler.cs new file mode 100644 index 0000000..24c88e4 --- /dev/null +++ b/apps/backend/Universe/Api/GetVersionHandler.cs @@ -0,0 +1,17 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Universe.Api; + +public sealed class GetVersionHandler(AppVersionService appVersionService) : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/api/version"); + AllowAnonymous(); + } + + public override async Task HandleAsync(CancellationToken cancellationToken) + { + await SendOkAsync(appVersionService.GetSnapshot(), cancellationToken); + } +} diff --git a/apps/backend/Universe/Api/ResetWorldHandler.cs b/apps/backend/Universe/Api/ResetWorldHandler.cs index f5ea4e0..be90405 100644 --- a/apps/backend/Universe/Api/ResetWorldHandler.cs +++ b/apps/backend/Universe/Api/ResetWorldHandler.cs @@ -7,7 +7,7 @@ public sealed class ResetWorldHandler(WorldService worldService) : EndpointWitho public override void Configure() { Post("/api/world/reset"); - AllowAnonymous(); + Policies(AuthPolicyNames.GmAccess); } public override Task HandleAsync(CancellationToken cancellationToken) => diff --git a/apps/backend/Universe/Api/SpawnShipHandler.cs b/apps/backend/Universe/Api/SpawnShipHandler.cs new file mode 100644 index 0000000..006c517 --- /dev/null +++ b/apps/backend/Universe/Api/SpawnShipHandler.cs @@ -0,0 +1,25 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Universe.Api; + +public sealed class SpawnShipHandler(WorldService worldService) : Endpoint +{ + public override void Configure() + { + Post("/api/gm/ships"); + Policies(AuthPolicyNames.GmAccess); + } + + public override async Task HandleAsync(SpawnShipCommandRequest request, CancellationToken cancellationToken) + { + try + { + await SendOkAsync(worldService.SpawnShip(request), cancellationToken); + } + catch (InvalidOperationException ex) + { + AddError(ex.Message); + await SendErrorsAsync(cancellation: cancellationToken); + } + } +} diff --git a/apps/backend/Universe/Api/SpawnStationHandler.cs b/apps/backend/Universe/Api/SpawnStationHandler.cs new file mode 100644 index 0000000..a369589 --- /dev/null +++ b/apps/backend/Universe/Api/SpawnStationHandler.cs @@ -0,0 +1,25 @@ +using FastEndpoints; + +namespace SpaceGame.Api.Universe.Api; + +public sealed class SpawnStationHandler(WorldService worldService) : Endpoint +{ + public override void Configure() + { + Post("/api/gm/stations"); + Policies(AuthPolicyNames.GmAccess); + } + + public override async Task HandleAsync(SpawnStationCommandRequest request, CancellationToken cancellationToken) + { + try + { + await SendOkAsync(worldService.SpawnStation(request), cancellationToken); + } + catch (InvalidOperationException ex) + { + AddError(ex.Message); + await SendErrorsAsync(cancellation: cancellationToken); + } + } +} diff --git a/apps/backend/Universe/Api/UpdateBalanceHandler.cs b/apps/backend/Universe/Api/UpdateBalanceHandler.cs index 05bc4d6..fbb900a 100644 --- a/apps/backend/Universe/Api/UpdateBalanceHandler.cs +++ b/apps/backend/Universe/Api/UpdateBalanceHandler.cs @@ -8,7 +8,7 @@ public sealed class UpdateBalanceHandler(IBalanceService balanceService) : Endpo public override void Configure() { Put("/api/balance"); - AllowAnonymous(); + Policies(AuthPolicyNames.GmAccess); } public override Task HandleAsync(BalanceOptions req, CancellationToken cancellationToken) diff --git a/apps/backend/Universe/Bootstrap/StaticDataProvider.cs b/apps/backend/Universe/Bootstrap/StaticDataProvider.cs index a1a2583..0612730 100644 --- a/apps/backend/Universe/Bootstrap/StaticDataProvider.cs +++ b/apps/backend/Universe/Bootstrap/StaticDataProvider.cs @@ -1,15 +1,22 @@ using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Extensions.Options; using SpaceGame.Api.Shared.Runtime; +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Universe.Bootstrap; public sealed class StaticDataProvider : IStaticDataProvider { + private const string MilitaryShipCategory = "military"; + private const string ConstructionShipCategory = "construction"; + private const string TransportShipCategory = "transport"; + private const string MiningShipCategory = "mining"; private readonly string _dataRoot; private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() }, }; public StaticDataProvider(IOptions staticDataOptions) @@ -163,7 +170,7 @@ public sealed class StaticDataProvider : IStaticDataProvider recipes.Add(new RecipeDefinition { Id = $"{ship.Id}-{production.Method}-construction", - Label = $"{ship.Label} Construction", + Label = $"{ship.Name} Construction", FacilityCategory = "shipyard", Duration = production.Time, Priority = InferShipRecipePriority(ship), @@ -224,12 +231,12 @@ public sealed class StaticDataProvider : IStaticDataProvider }; private static int InferShipRecipePriority(ShipDefinition ship) => - ship.Kind switch + GetShipCategory(ship) switch { - "military" => 170, - "construction" => 140, - "transport" => 120, - "mining" => 110, + MilitaryShipCategory => 170, + ConstructionShipCategory => 140, + TransportShipCategory => 120, + MiningShipCategory => 110, _ => 100, }; diff --git a/apps/backend/Universe/Contracts/GmCommands.cs b/apps/backend/Universe/Contracts/GmCommands.cs new file mode 100644 index 0000000..1cee44b --- /dev/null +++ b/apps/backend/Universe/Contracts/GmCommands.cs @@ -0,0 +1,16 @@ +namespace SpaceGame.Api.Universe.Contracts; + +public sealed record CreateFactionCommandRequest( + string FactionId); + +public sealed record SpawnShipCommandRequest( + string FactionId, + string SystemId, + string? ShipId = null, + string? BehaviorKind = null); + +public sealed record SpawnStationCommandRequest( + string FactionId, + string SystemId, + string? Objective = null, + string? Label = null); diff --git a/apps/backend/Universe/Contracts/World.cs b/apps/backend/Universe/Contracts/World.cs index 8e7baf1..9e81b18 100644 --- a/apps/backend/Universe/Contracts/World.cs +++ b/apps/backend/Universe/Contracts/World.cs @@ -18,7 +18,6 @@ public sealed record WorldSnapshot( IReadOnlyList Policies, IReadOnlyList Ships, IReadOnlyList Factions, - PlayerFactionSnapshot? PlayerFaction, GeopoliticalStateSnapshot? Geopolitics); public sealed record WorldDelta( @@ -38,7 +37,6 @@ public sealed record WorldDelta( IReadOnlyList Policies, IReadOnlyList Ships, IReadOnlyList Factions, - PlayerFactionSnapshot? PlayerFaction, GeopoliticalStateSnapshot? Geopolitics, ObserverScope? Scope = null); diff --git a/apps/backend/Universe/Scenario/LoaderSupport.cs b/apps/backend/Universe/Scenario/LoaderSupport.cs index 305ba45..60f1d07 100644 --- a/apps/backend/Universe/Scenario/LoaderSupport.cs +++ b/apps/backend/Universe/Scenario/LoaderSupport.cs @@ -89,9 +89,6 @@ internal static class LoaderSupport internal static bool HasInstalledModules(StationRuntime station, params string[] modules) => modules.All(moduleId => station.Modules.Any(candidate => string.Equals(candidate.ModuleId, moduleId, StringComparison.Ordinal))); - internal static bool HasCapabilities(ShipDefinition definition, params string[] capabilities) => - capabilities.All(capability => definition.Capabilities.Contains(capability, StringComparer.Ordinal)); - internal static void AddStationModule(StationRuntime station, IReadOnlyDictionary moduleDefinitions, string moduleId) { if (!moduleDefinitions.TryGetValue(moduleId, out var definition)) diff --git a/apps/backend/Universe/Scenario/ScenarioContentBuilder.cs b/apps/backend/Universe/Scenario/ScenarioContentBuilder.cs index 1ec754c..2474413 100644 --- a/apps/backend/Universe/Scenario/ScenarioContentBuilder.cs +++ b/apps/backend/Universe/Scenario/ScenarioContentBuilder.cs @@ -1,5 +1,7 @@ using SpaceGame.Api.Universe.Bootstrap; -using SpaceGame.Api.Ships.Simulation; +using SpaceGame.Api.Ships.AI; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; using static SpaceGame.Api.Universe.Scenario.LoaderSupport; namespace SpaceGame.Api.Universe.Scenario; @@ -194,7 +196,7 @@ public sealed class ScenarioContentBuilder( patrolRoutes, stations), Skills = ShipBootstrapPolicy.CreateSkills(definition), - Health = definition.MaxHealth, + Health = definition.Hull, }); foreach (var (itemId, amount) in formation.StartingInventory) @@ -232,45 +234,45 @@ public sealed class ScenarioContentBuilder( && string.Equals(station.SystemId, systemId, StringComparison.Ordinal)) ?? stations.FirstOrDefault(station => string.Equals(station.FactionId, factionId, StringComparison.Ordinal)); - if (string.Equals(definition.Kind, "construction", StringComparison.Ordinal) && homeStation is not null) + if (IsConstructionShip(definition) && homeStation is not null) { return new DefaultBehaviorRuntime { - Kind = "construct-station", + Kind = ConstructStation, HomeSystemId = homeStation.SystemId, HomeStationId = homeStation.Id, PreferredConstructionSiteId = null, }; } - if (LoaderSupport.HasCapabilities(definition, "mining") && homeStation is not null) + if (IsMiningShip(definition) && homeStation is not null) { return new DefaultBehaviorRuntime { - Kind = definition.CargoCapacity >= 120f ? "expert-auto-mine" : "advanced-auto-mine", + Kind = definition.GetTotalCargoCapacity() >= 120f ? ExpertAutoMine : AdvancedAutoMine, HomeSystemId = homeStation.SystemId, HomeStationId = homeStation.Id, AreaSystemId = homeStation.SystemId, - MaxSystemRange = definition.CargoCapacity >= 120f ? 3 : 1, + MaxSystemRange = definition.GetTotalCargoCapacity() >= 120f ? 3 : 1, }; } - if (string.Equals(definition.Kind, "transport", StringComparison.Ordinal)) + if (IsTransportShip(definition)) { return new DefaultBehaviorRuntime { - Kind = "advanced-auto-trade", + Kind = AdvancedAutoTrade, HomeSystemId = homeStation?.SystemId ?? systemId, HomeStationId = homeStation?.Id, MaxSystemRange = 2, }; } - if (string.Equals(definition.Kind, "military", StringComparison.Ordinal) && patrolRoutes.TryGetValue(systemId, out var route)) + if (IsMilitaryShip(definition) && patrolRoutes.TryGetValue(systemId, out var route)) { return new DefaultBehaviorRuntime { - Kind = "patrol", + Kind = Patrol, HomeSystemId = homeStation?.SystemId ?? systemId, HomeStationId = homeStation?.Id, AreaSystemId = systemId, @@ -281,9 +283,10 @@ public sealed class ScenarioContentBuilder( return new DefaultBehaviorRuntime { - Kind = "idle", + Kind = HoldPosition, HomeSystemId = homeStation?.SystemId ?? systemId, HomeStationId = homeStation?.Id, + AreaSystemId = homeStation?.SystemId ?? systemId, }; } } diff --git a/apps/backend/Universe/Scenario/SystemGenerationService.cs b/apps/backend/Universe/Scenario/SystemGenerationService.cs index c217380..8166883 100644 --- a/apps/backend/Universe/Scenario/SystemGenerationService.cs +++ b/apps/backend/Universe/Scenario/SystemGenerationService.cs @@ -520,6 +520,8 @@ public sealed class SystemGenerationService private static float Jitter(int index, int salt, float amplitude) => (Hash01(index, salt) * 2f - 1f) * amplitude; + // Cheap deterministic pseudo-random helper: same (index, salt) pair always maps to the same 0..1 value. + // Generation code uses it instead of a mutable RNG so each procedural choice stays stable for a given seed. private static float Hash01(int index, int salt) { uint value = (uint)(index + 1); diff --git a/apps/backend/Universe/Scenario/WorldRuntimeAssembler.cs b/apps/backend/Universe/Scenario/WorldRuntimeAssembler.cs index 1e5a820..22ea259 100644 --- a/apps/backend/Universe/Scenario/WorldRuntimeAssembler.cs +++ b/apps/backend/Universe/Scenario/WorldRuntimeAssembler.cs @@ -18,9 +18,6 @@ public sealed class WorldRuntimeAssembler( var policies = seedingService.CreatePolicies(factions); var commanders = seedingService.CreateCommanders(factions, content.Stations, content.Ships); var nowUtc = DateTimeOffset.UtcNow; - var playerFaction = worldGenerationOptions.GeneratePlayerFaction - ? seedingService.CreatePlayerFaction(factions, content.Stations, content.Ships, commanders, policies, nowUtc) - : null; var claims = seedingService.CreateClaims(content.Stations, topology.SpatialLayout.Celestials, nowUtc); var world = new SimulationWorld @@ -34,7 +31,6 @@ public sealed class WorldRuntimeAssembler( Stations = content.Stations.ToList(), Ships = content.Ships.ToList(), Factions = factions, - PlayerFaction = playerFaction, Geopolitics = null, Commanders = commanders, Claims = claims, diff --git a/apps/backend/Universe/Scenario/WorldSeedingService.cs b/apps/backend/Universe/Scenario/WorldSeedingService.cs index 1f1c9e2..1ca359a 100644 --- a/apps/backend/Universe/Scenario/WorldSeedingService.cs +++ b/apps/backend/Universe/Scenario/WorldSeedingService.cs @@ -1,4 +1,5 @@ using SpaceGame.Api.Universe.Bootstrap; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; using static SpaceGame.Api.Universe.Scenario.LoaderSupport; namespace SpaceGame.Api.Universe.Scenario; @@ -379,7 +380,7 @@ public sealed class WorldSeedingService(IStaticDataProvider staticData) Label = "Core Automation", ScopeKind = "player-faction", ScopeId = player.Id, - BehaviorKind = "idle", + BehaviorKind = Idle, UpdatedAtUtc = nowUtc, }); @@ -395,7 +396,7 @@ public sealed class WorldSeedingService(IStaticDataProvider staticData) return player; } - private FactionRuntime CreateFaction(string factionId) + internal FactionRuntime CreateFaction(string factionId) { if (!staticData.FactionDefinitions.TryGetValue(factionId, out var definition)) { diff --git a/apps/backend/Universe/Simulation/WorldService.cs b/apps/backend/Universe/Simulation/WorldService.cs index 0edfc34..843b96e 100644 --- a/apps/backend/Universe/Simulation/WorldService.cs +++ b/apps/backend/Universe/Simulation/WorldService.cs @@ -1,6 +1,9 @@ using System.Threading.Channels; using Microsoft.Extensions.Options; +using SpaceGame.Api.Universe.Bootstrap; using SpaceGame.Api.Universe.Scenario; +using static SpaceGame.Api.Shared.Runtime.ShipBehaviorKinds; +using static SpaceGame.Api.Shared.Runtime.SimulationRuntimeSupport; namespace SpaceGame.Api.Universe.Simulation; @@ -11,8 +14,13 @@ public sealed class WorldService private readonly Lock _sync = new(); private readonly OrbitalSimulationSnapshot _orbitalSimulation; private readonly SimulationEngine _engine; + private readonly IPlayerIdentityResolver _playerIdentityResolver; + private readonly IPlayerStateStore _playerStateStore; + private readonly PlayerFactionProjectionService _playerFactionProjection; private readonly ScenarioLoader _scenarioLoader; private readonly WorldBuilder _worldBuilder; + private readonly IStaticDataProvider _staticData; + private readonly WorldSeedingService _worldSeedingService; private readonly PlayerFactionService _playerFaction = new(); private readonly Dictionary _subscribers = []; private readonly Queue _history = []; @@ -24,13 +32,23 @@ public sealed class WorldService public WorldService( ScenarioLoader scenarioLoader, WorldBuilder worldBuilder, + IStaticDataProvider staticData, + WorldSeedingService worldSeedingService, + IPlayerStateStore playerStateStore, + IPlayerIdentityResolver playerIdentityResolver, + PlayerFactionProjectionService playerFactionProjection, IBalanceService balance, IOptions orbitalSimulationOptions) { _orbitalSimulation = new OrbitalSimulationSnapshot(orbitalSimulationOptions.Value.SimulatedSecondsPerRealSecond); + _playerStateStore = playerStateStore; + _playerIdentityResolver = playerIdentityResolver; + _playerFactionProjection = playerFactionProjection; _scenarioLoader = scenarioLoader; _worldBuilder = worldBuilder; - _engine = new SimulationEngine(orbitalSimulationOptions.Value, balance); + _staticData = staticData; + _worldSeedingService = worldSeedingService; + _engine = new SimulationEngine(orbitalSimulationOptions.Value, balance, playerStateStore); } public void New(WorldGenerationOptions options) @@ -81,7 +99,10 @@ public sealed class WorldService { lock (_sync) { - var ship = _playerFaction.EnqueueDirectShipOrder(_world, shipId, request); + ValidateShipOrderRequestUnsafe(shipId, request); + var ship = CanCurrentActorAccessGm() + ? EnqueueGmShipOrderUnsafe(shipId, request) + : _playerFaction.EnqueueDirectShipOrder(_world, _playerStateStore, GetCurrentPlayerKey(), shipId, request); if (ship is null) { return null; @@ -95,7 +116,9 @@ public sealed class WorldService { lock (_sync) { - var ship = _playerFaction.RemoveDirectShipOrder(_world, shipId, orderId); + var ship = CanCurrentActorAccessGm() + ? RemoveGmShipOrderUnsafe(shipId, orderId) + : _playerFaction.RemoveDirectShipOrder(_world, _playerStateStore, GetCurrentPlayerKey(), shipId, orderId); if (ship is null) { return null; @@ -109,7 +132,9 @@ public sealed class WorldService { lock (_sync) { - var ship = _playerFaction.ConfigureDirectShipBehavior(_world, shipId, request); + var ship = CanCurrentActorAccessGm() + ? ConfigureGmShipBehaviorUnsafe(shipId, request) + : _playerFaction.ConfigureDirectShipBehavior(_world, _playerStateStore, GetCurrentPlayerKey(), shipId, request); if (ship is null) { return null; @@ -123,13 +148,15 @@ public sealed class WorldService { lock (_sync) { - if (_world.PlayerFaction is null && _world.Factions.Count == 0) + if (_world.Factions.Count == 0) { return null; } - _playerFaction.EnsureDomain(_world); - return GetPlayerFactionSnapshotUnsafe(); + var playerKey = GetCurrentPlayerKey(); + var player = _playerFaction.TryGetDomain(_playerStateStore, playerKey) + ?? _playerFaction.EnsureDomain(_world, _playerStateStore, playerKey); + return _playerFactionProjection.ToSnapshot(player); } } @@ -137,7 +164,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.CreateOrganization(_world, request); + _playerFaction.CreateOrganization(_world, _playerStateStore, GetCurrentPlayerKey(), request); return GetPlayerFactionSnapshotUnsafe(); } } @@ -146,7 +173,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.DeleteOrganization(_world, organizationId); + _playerFaction.DeleteOrganization(_world, _playerStateStore, GetCurrentPlayerKey(), organizationId); return GetPlayerFactionSnapshotUnsafe(); } } @@ -155,7 +182,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.UpdateOrganizationMembership(_world, organizationId, request); + _playerFaction.UpdateOrganizationMembership(_world, _playerStateStore, GetCurrentPlayerKey(), organizationId, request); return GetPlayerFactionSnapshotUnsafe(); } } @@ -164,7 +191,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.UpsertDirective(_world, directiveId, request); + _playerFaction.UpsertDirective(_world, _playerStateStore, GetCurrentPlayerKey(), directiveId, request); return GetPlayerFactionSnapshotUnsafe(); } } @@ -173,7 +200,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.DeleteDirective(_world, directiveId); + _playerFaction.DeleteDirective(_world, _playerStateStore, GetCurrentPlayerKey(), directiveId); return GetPlayerFactionSnapshotUnsafe(); } } @@ -182,7 +209,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.UpsertPolicy(_world, policyId, request); + _playerFaction.UpsertPolicy(_world, _playerStateStore, GetCurrentPlayerKey(), policyId, request); return GetPlayerFactionSnapshotUnsafe(); } } @@ -191,7 +218,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.UpsertAutomationPolicy(_world, automationPolicyId, request); + _playerFaction.UpsertAutomationPolicy(_world, _playerStateStore, GetCurrentPlayerKey(), automationPolicyId, request); return GetPlayerFactionSnapshotUnsafe(); } } @@ -200,7 +227,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.UpsertReinforcementPolicy(_world, reinforcementPolicyId, request); + _playerFaction.UpsertReinforcementPolicy(_world, _playerStateStore, GetCurrentPlayerKey(), reinforcementPolicyId, request); return GetPlayerFactionSnapshotUnsafe(); } } @@ -209,7 +236,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.UpsertProductionProgram(_world, productionProgramId, request); + _playerFaction.UpsertProductionProgram(_world, _playerStateStore, GetCurrentPlayerKey(), productionProgramId, request); return GetPlayerFactionSnapshotUnsafe(); } } @@ -218,7 +245,7 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.UpsertAssignment(_world, assetId, request); + _playerFaction.UpsertAssignment(_world, _playerStateStore, GetCurrentPlayerKey(), assetId, request); return GetPlayerFactionSnapshotUnsafe(); } } @@ -227,11 +254,118 @@ public sealed class WorldService { lock (_sync) { - _playerFaction.UpdateStrategicIntent(_world, request); + _playerFaction.UpdateStrategicIntent(_world, _playerStateStore, GetCurrentPlayerKey(), request); return GetPlayerFactionSnapshotUnsafe(); } } + public FactionSnapshot CreateFaction(string factionId) + { + lock (_sync) + { + if (_world.Factions.Any(candidate => string.Equals(candidate.Id, factionId, StringComparison.Ordinal))) + { + throw new InvalidOperationException($"Faction '{factionId}' already exists in the current world."); + } + + var faction = _worldSeedingService.CreateFaction(factionId); + _world.Factions.Add(faction); + + var policy = _worldSeedingService.CreatePolicies([faction]).Single(); + _world.Policies.Add(policy); + + var factionCommander = CreateFactionCommander(faction); + _world.Commanders.Add(factionCommander); + faction.CommanderIds.Add(factionCommander.Id); + + new GeopoliticalSimulationService().Update(_world, 0f, []); + PublishSnapshotRefreshUnsafe("create-faction", $"Created faction {factionId}", "faction", factionId); + return _engine.BuildSnapshot(_world, _sequence).Factions.First(candidate => candidate.Id == factionId); + } + } + + public ShipSnapshot SpawnShip(SpawnShipCommandRequest request) + { + lock (_sync) + { + var faction = _world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, request.FactionId, StringComparison.Ordinal)) + ?? throw new InvalidOperationException($"Faction '{request.FactionId}' does not exist in the current world."); + var system = _world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, request.SystemId, StringComparison.Ordinal)) + ?? throw new InvalidOperationException($"System '{request.SystemId}' does not exist in the current world."); + var definition = ResolveShipDefinition(request, faction.Id); + var shipId = $"ship-{faction.Id}-{definition.Id}-{Guid.NewGuid():N}".ToLowerInvariant(); + var spawnPosition = ResolveSpawnPosition(system.Definition.Id); + var homeStation = _world.Stations.FirstOrDefault(candidate => + string.Equals(candidate.FactionId, faction.Id, StringComparison.Ordinal) + && string.Equals(candidate.SystemId, system.Definition.Id, StringComparison.Ordinal)); + var defaultBehavior = CreateSpawnBehavior(request, definition, system.Definition.Id, homeStation); + + var ship = new ShipRuntime + { + Id = shipId, + SystemId = system.Definition.Id, + Definition = definition, + FactionId = faction.Id, + Position = spawnPosition, + TargetPosition = spawnPosition, + SpatialState = SpatialBuilder.CreateInitialShipSpatialState(system.Definition.Id, spawnPosition, _world.Celestials), + DefaultBehavior = defaultBehavior, + Skills = ShipBootstrapPolicy.CreateSkills(definition), + Health = definition.Hull, + }; + + _world.Ships.Add(ship); + EnsureShipCommander(faction, ship); + new GeopoliticalSimulationService().Update(_world, 0f, []); + PublishSnapshotRefreshUnsafe("spawn-ship", $"Spawned ship {ship.Id}", "ship", ship.Id); + return GetShipSnapshotUnsafe(ship.Id) + ?? throw new InvalidOperationException($"Ship '{ship.Id}' could not be projected."); + } + } + + public StationSnapshot SpawnStation(SpawnStationCommandRequest request) + { + lock (_sync) + { + var faction = _world.Factions.FirstOrDefault(candidate => string.Equals(candidate.Id, request.FactionId, StringComparison.Ordinal)) + ?? throw new InvalidOperationException($"Faction '{request.FactionId}' does not exist in the current world."); + var system = _world.Systems.FirstOrDefault(candidate => string.Equals(candidate.Definition.Id, request.SystemId, StringComparison.Ordinal)) + ?? throw new InvalidOperationException($"System '{request.SystemId}' does not exist in the current world."); + var objective = StationSimulationService.NormalizeStationObjective(request.Objective); + var label = string.IsNullOrWhiteSpace(request.Label) + ? $"{faction.Label} {ToTitleCaseToken(objective)} {CountFactionStationsInSystem(faction.Id, system.Definition.Id) + 1}" + : request.Label.Trim(); + var stationId = $"station-{faction.Id}-{objective}-{Guid.NewGuid():N}".ToLowerInvariant(); + var position = ResolveStationSpawnPosition(system.Definition.Id); + var station = new StationRuntime + { + Id = stationId, + SystemId = system.Definition.Id, + Label = label, + Color = faction.Color, + Objective = objective, + Position = position, + FactionId = faction.Id, + PolicySetId = faction.DefaultPolicySetId, + Health = 600f, + MaxHealth = 600f, + }; + + foreach (var moduleId in BuildStarterStationModules(faction.Id, objective)) + { + AddStationModule(_world, station, moduleId); + } + + station.PopulationCapacity = GetStationSupportedPopulation(_world.ModuleDefinitions, station); + station.WorkforceRequired = GetStationRequiredWorkforce(_world.ModuleDefinitions, station); + _world.Stations.Add(station); + + new GeopoliticalSimulationService().Update(_world, 0f, []); + PublishSnapshotRefreshUnsafe("spawn-station", $"Spawned station {station.Id}", "station", station.Id); + return _engine.BuildSnapshot(_world, _sequence).Stations.First(candidate => candidate.Id == station.Id); + } + } + public ChannelReader Subscribe(ObserverScope scope, long afterSequence, CancellationToken cancellationToken) { var channel = Channel.CreateUnbounded(new UnboundedChannelOptions @@ -318,6 +452,7 @@ public sealed class WorldService private void ReplaceWorldUnsafe(SimulationWorld world, string eventKind, string eventMessage) { _world = world; + _playerStateStore.Clear(); _sequence += 1; _history.Clear(); @@ -339,7 +474,6 @@ public sealed class WorldService [], [], [], - null, null); _history.Enqueue(worldDelta); @@ -349,11 +483,431 @@ public sealed class WorldService } } + private void PublishSnapshotRefreshUnsafe( + string eventKind, + string eventMessage, + string entityKind, + string entityId, + string scopeKind = "universe", + string? scopeEntityId = null) + { + _sequence += 1; + var eventTime = DateTimeOffset.UtcNow; + var worldDelta = new WorldDelta( + _sequence, + _world.TickIntervalMs, + _world.OrbitalTimeSeconds, + _orbitalSimulation, + eventTime, + true, + [new SimulationEventRecord(entityKind, entityId, eventKind, eventMessage, eventTime, "world", scopeKind, scopeEntityId)], + [], + [], + [], + [], + [], + [], + [], + [], + [], + null); + + _history.Enqueue(worldDelta); + while (_history.Count > DeltaHistoryLimit) + { + _history.Dequeue(); + } + + foreach (var subscriber in _subscribers.Values.ToList()) + { + subscriber.Channel.Writer.TryWrite(FilterDeltaForScope(worldDelta, subscriber.Scope)); + } + } + private ShipSnapshot? GetShipSnapshotUnsafe(string shipId) => _engine.BuildSnapshot(_world, _sequence).Ships.FirstOrDefault(ship => ship.Id == shipId); private PlayerFactionSnapshot? GetPlayerFactionSnapshotUnsafe() => - _engine.BuildSnapshot(_world, _sequence).PlayerFaction; + _playerFactionProjection.ToSnapshot(_playerFaction.TryGetDomain(_playerStateStore, GetCurrentPlayerKey())); + + private string GetCurrentPlayerKey() => _playerIdentityResolver.GetRequiredPlayerId().ToString("N"); + + private bool CanCurrentActorAccessGm() => _playerIdentityResolver.CanAccessGm(); + + private string GetCurrentActorSourceId() => + _playerIdentityResolver.GetCurrentPlayerId()?.ToString("N") ?? "gm"; + + private void ValidateShipOrderRequestUnsafe(string shipId, ShipOrderCommandRequest request) + { + var ship = _world.Ships.FirstOrDefault(candidate => candidate.Id == shipId) + ?? throw new InvalidOperationException($"Ship '{shipId}' was not found."); + + if (!string.Equals(request.Kind, ShipOrderKinds.MineAndDeliver, StringComparison.Ordinal)) + { + return; + } + + if (!IsMiningShip(ship.Definition)) + { + throw new InvalidOperationException($"{ship.Definition.Name} cannot accept Mine Resource because it does not have mining capability."); + } + + if (string.IsNullOrWhiteSpace(request.ItemId)) + { + throw new InvalidOperationException("Mine Resource requires a ware."); + } + + if (!_world.ItemDefinitions.TryGetValue(request.ItemId, out var itemDefinition)) + { + throw new InvalidOperationException($"Mine Resource references unknown ware '{request.ItemId}'."); + } + + if (itemDefinition.CargoKind is null) + { + throw new InvalidOperationException($"Mine Resource ware '{request.ItemId}' is not mineable."); + } + + if (!ship.Definition.SupportsCargoKind(itemDefinition.CargoKind.Value)) + { + throw new InvalidOperationException($"{ship.Definition.Name} cannot mine '{request.ItemId}' because it cannot store '{itemDefinition.CargoKind.Value.ToDataValue()}'."); + } + } + + private ShipRuntime? EnqueueGmShipOrderUnsafe(string shipId, ShipOrderCommandRequest request) + { + var ship = _world.Ships.FirstOrDefault(candidate => candidate.Id == shipId); + if (ship is null) + { + return null; + } + + if (ship.OrderQueue.Count >= 8) + { + throw new InvalidOperationException("Order queue is full."); + } + + ship.OrderQueue.Add(new ShipOrderRuntime + { + Id = $"order-{ship.Id}-{Guid.NewGuid():N}", + Kind = request.Kind, + SourceKind = ShipOrderSourceKind.Player, + SourceId = GetCurrentActorSourceId(), + Priority = request.Priority, + InterruptCurrentPlan = request.InterruptCurrentPlan, + Label = request.Label, + TargetEntityId = request.TargetEntityId, + TargetSystemId = request.TargetSystemId, + TargetPosition = request.TargetPosition is null ? null : new Vector3(request.TargetPosition.X, request.TargetPosition.Y, request.TargetPosition.Z), + SourceStationId = request.SourceStationId, + DestinationStationId = request.DestinationStationId, + ItemId = request.ItemId, + NodeId = request.NodeId, + ConstructionSiteId = request.ConstructionSiteId, + ModuleId = request.ModuleId, + WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? 0f), + Radius = MathF.Max(0f, request.Radius ?? 0f), + MaxSystemRange = request.MaxSystemRange, + KnownStationsOnly = request.KnownStationsOnly ?? false, + }); + + ship.ControlSourceKind = "gm-order"; + ship.ControlSourceId = ship.OrderQueue + .Where(order => order.SourceKind == ShipOrderSourceKind.Player) + .OrderByDescending(order => order.Priority) + .ThenBy(order => order.CreatedAtUtc) + .Select(order => order.Id) + .FirstOrDefault(); + ship.ControlReason = request.Label ?? request.Kind; + ship.NeedsReplan = true; + ship.LastReplanReason = "gm-order-enqueued"; + ship.LastDeltaSignature = string.Empty; + return ship; + } + + private ShipRuntime? RemoveGmShipOrderUnsafe(string shipId, string orderId) + { + var ship = _world.Ships.FirstOrDefault(candidate => candidate.Id == shipId); + if (ship is null) + { + return null; + } + + ship.OrderQueue.RemoveAll(order => order.Id == orderId); + ship.ControlSourceKind = ship.OrderQueue.Any(order => order.SourceKind == ShipOrderSourceKind.Player) + ? "gm-order" + : "gm-manual"; + ship.ControlSourceId = ship.OrderQueue + .Where(order => order.SourceKind == ShipOrderSourceKind.Player) + .OrderByDescending(order => order.Priority) + .ThenBy(order => order.CreatedAtUtc) + .Select(order => order.Id) + .FirstOrDefault(); + ship.ControlReason = ship.OrderQueue + .Where(order => order.SourceKind == ShipOrderSourceKind.Player) + .OrderByDescending(order => order.Priority) + .ThenBy(order => order.CreatedAtUtc) + .Select(order => order.Label ?? order.Kind) + .FirstOrDefault() + ?? "manual-gm-control"; + ship.NeedsReplan = true; + ship.LastReplanReason = "gm-order-removed"; + ship.LastDeltaSignature = string.Empty; + return ship; + } + + private ShipRuntime? ConfigureGmShipBehaviorUnsafe(string shipId, ShipDefaultBehaviorCommandRequest request) + { + var ship = _world.Ships.FirstOrDefault(candidate => candidate.Id == shipId); + if (ship is null) + { + return null; + } + + ship.DefaultBehavior.Kind = request.Kind; + ship.DefaultBehavior.HomeSystemId = request.HomeSystemId ?? ship.SystemId; + ship.DefaultBehavior.HomeStationId = request.HomeStationId; + ship.DefaultBehavior.AreaSystemId = request.AreaSystemId; + ship.DefaultBehavior.TargetEntityId = request.TargetEntityId; + ship.DefaultBehavior.ItemId = request.ItemId; + ship.DefaultBehavior.PreferredNodeId = request.PreferredNodeId; + ship.DefaultBehavior.PreferredConstructionSiteId = request.PreferredConstructionSiteId; + ship.DefaultBehavior.PreferredModuleId = request.PreferredModuleId; + ship.DefaultBehavior.TargetPosition = request.TargetPosition is null + ? null + : new Vector3(request.TargetPosition.X, request.TargetPosition.Y, request.TargetPosition.Z); + ship.DefaultBehavior.WaitSeconds = MathF.Max(0f, request.WaitSeconds ?? ship.DefaultBehavior.WaitSeconds); + ship.DefaultBehavior.Radius = MathF.Max(0f, request.Radius ?? ship.DefaultBehavior.Radius); + ship.DefaultBehavior.MaxSystemRange = Math.Max(0, request.MaxSystemRange ?? ship.DefaultBehavior.MaxSystemRange); + ship.DefaultBehavior.KnownStationsOnly = request.KnownStationsOnly ?? ship.DefaultBehavior.KnownStationsOnly; + ship.DefaultBehavior.PatrolPoints = + (request.PatrolPoints ?? []) + .Select(point => new Vector3(point.X, point.Y, point.Z)) + .ToList(); + ship.DefaultBehavior.PatrolIndex = 0; + ship.DefaultBehavior.RepeatOrders = + (request.RepeatOrders ?? []) + .Select(template => new ShipOrderTemplateRuntime + { + Kind = template.Kind, + Label = template.Label, + TargetEntityId = template.TargetEntityId, + TargetSystemId = template.TargetSystemId, + TargetPosition = template.TargetPosition is null ? null : new Vector3(template.TargetPosition.X, template.TargetPosition.Y, template.TargetPosition.Z), + SourceStationId = template.SourceStationId, + DestinationStationId = template.DestinationStationId, + ItemId = template.ItemId, + NodeId = template.NodeId, + ConstructionSiteId = template.ConstructionSiteId, + ModuleId = template.ModuleId, + WaitSeconds = template.WaitSeconds ?? 0f, + Radius = template.Radius ?? 0f, + MaxSystemRange = template.MaxSystemRange, + KnownStationsOnly = template.KnownStationsOnly ?? false, + }) + .ToList(); + ship.DefaultBehavior.RepeatIndex = 0; + + ship.ControlSourceKind = "gm-manual"; + ship.ControlSourceId = GetCurrentActorSourceId(); + ship.ControlReason = request.Kind; + ship.NeedsReplan = true; + ship.LastReplanReason = "gm-behavior-updated"; + ship.LastDeltaSignature = string.Empty; + return ship; + } + + private CommanderRuntime CreateFactionCommander(FactionRuntime faction) => new() + { + Id = $"commander-faction-{faction.Id}", + Kind = CommanderKind.Faction, + FactionId = faction.Id, + ControlledEntityId = faction.Id, + PolicySetId = faction.DefaultPolicySetId, + Doctrine = "strategic-control", + }; + + private void EnsureShipCommander(FactionRuntime faction, ShipRuntime ship) + { + var factionCommander = _world.Commanders.FirstOrDefault(candidate => + string.Equals(candidate.Kind, CommanderKind.Faction, StringComparison.Ordinal) + && string.Equals(candidate.FactionId, faction.Id, StringComparison.Ordinal)); + if (factionCommander is null) + { + return; + } + + var commander = new CommanderRuntime + { + Id = $"commander-ship-{ship.Id}", + Kind = CommanderKind.Ship, + FactionId = faction.Id, + ParentCommanderId = factionCommander.Id, + ControlledEntityId = ship.Id, + PolicySetId = factionCommander.PolicySetId, + Doctrine = "ship-control", + Skills = new CommanderSkillProfileRuntime + { + Leadership = Math.Clamp((ship.Skills.Navigation + ship.Skills.Combat + 1) / 2, 2, 5), + Coordination = Math.Clamp((ship.Skills.Trade + ship.Skills.Mining + 1) / 2, 2, 5), + Strategy = Math.Clamp((ship.Skills.Combat + ship.Skills.Construction + 1) / 2, 2, 5), + }, + }; + + ship.CommanderId = commander.Id; + ship.PolicySetId = factionCommander.PolicySetId; + factionCommander.SubordinateCommanderIds.Add(commander.Id); + faction.CommanderIds.Add(commander.Id); + _world.Commanders.Add(commander); + } + + private ShipDefinition ResolveShipDefinition(SpawnShipCommandRequest request, string factionId) + { + if (!string.IsNullOrWhiteSpace(request.ShipId)) + { + return _staticData.ShipDefinitions.TryGetValue(request.ShipId, out var explicitDefinition) + ? explicitDefinition + : throw new InvalidOperationException($"Ship '{request.ShipId}' is not defined in static data."); + } + + return _staticData.ShipDefinitions.Values + .Where(IsMiningShip) + .OrderBy(definition => !definition.Owners.Contains(factionId, StringComparer.Ordinal)) + .ThenBy(definition => !definition.SupportsCargoKind(StorageKind.Solid)) + .ThenBy(definition => definition.Size != "small") + .ThenBy(definition => definition.Id, StringComparer.Ordinal) + .FirstOrDefault() + ?? throw new InvalidOperationException("No mining ship definition is available in static data."); + } + + private Vector3 ResolveSpawnPosition(string systemId) + { + var shipsInSystem = _world.Ships.Count(candidate => string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal)); + var angle = shipsInSystem * 0.73f; + return new Vector3(60f + (shipsInSystem * 12f), 0f, MathF.Sin(angle) * 34f); + } + + private Vector3 ResolveStationSpawnPosition(string systemId) + { + var stationsInSystem = _world.Stations.Count(candidate => string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal)); + var angle = stationsInSystem * 0.91f; + var radius = 160f + (stationsInSystem * 42f); + return new Vector3(MathF.Cos(angle) * radius, 0f, MathF.Sin(angle) * radius); + } + + private IReadOnlyList BuildStarterStationModules(string factionId, string objective) + { + var modules = new List(); + + EnsureStationModule(modules, StarterStationLayoutResolver.ResolveDockModuleId(factionId, _staticData.ModuleDefinitions)); + + var powerModuleId = StarterStationLayoutResolver.ResolvePowerModuleId(factionId, _staticData.ModuleDefinitions); + EnsureStationModule(modules, powerModuleId); + + var defaultContainerStorageModuleId = StarterStationLayoutResolver.ResolveRequiredStorageModuleIds( + powerModuleId, + factionId, + _staticData.ModuleDefinitions, + _staticData.ItemDefinitions) + .FirstOrDefault(moduleId => + { + return _staticData.ModuleDefinitions.TryGetValue(moduleId, out var definition) + && definition is StorageModuleDefinition storageDefinition + && storageDefinition.StorageKind == StorageKind.Container; + }); + + if (defaultContainerStorageModuleId is not null) + { + EnsureStationModule(modules, defaultContainerStorageModuleId); + } + + var objectiveModuleId = StarterStationLayoutResolver.ResolveObjectiveModuleId(objective, factionId, _staticData.ModuleDefinitions); + if (!string.IsNullOrWhiteSpace(objectiveModuleId)) + { + EnsureStationModule(modules, objectiveModuleId); + foreach (var storageModuleId in StarterStationLayoutResolver.ResolveRequiredStorageModuleIds( + objectiveModuleId, + factionId, + _staticData.ModuleDefinitions, + _staticData.ItemDefinitions)) + { + EnsureStationModule(modules, storageModuleId); + } + } + + return modules; + } + + private static void EnsureStationModule(List modules, string moduleId) + { + if (!modules.Contains(moduleId, StringComparer.Ordinal)) + { + modules.Add(moduleId); + } + } + + private int CountFactionStationsInSystem(string factionId, string systemId) => + _world.Stations.Count(candidate => + string.Equals(candidate.FactionId, factionId, StringComparison.Ordinal) + && string.Equals(candidate.SystemId, systemId, StringComparison.Ordinal)); + + private static string ToTitleCaseToken(string value) => + string.Join(" ", + value + .Split(['-', '_', ' '], StringSplitOptions.RemoveEmptyEntries) + .Select(part => part.Length == 0 ? part : char.ToUpperInvariant(part[0]) + part[1..])); + + private static DefaultBehaviorRuntime CreateSpawnBehavior( + SpawnShipCommandRequest request, + ShipDefinition definition, + string systemId, + StationRuntime? homeStation) + { + var requestedBehavior = request.BehaviorKind?.Trim(); + if (!string.IsNullOrWhiteSpace(requestedBehavior)) + { + return new DefaultBehaviorRuntime + { + Kind = requestedBehavior, + HomeSystemId = systemId, + HomeStationId = homeStation?.Id, + AreaSystemId = systemId, + ItemId = string.Equals(requestedBehavior, LocalAutoMine, StringComparison.Ordinal) ? "ore" : null, + }; + } + + if (IsMiningShip(definition) && homeStation is not null) + { + return new DefaultBehaviorRuntime + { + Kind = LocalAutoMine, + HomeSystemId = systemId, + HomeStationId = homeStation.Id, + AreaSystemId = systemId, + }; + } + + if (IsMiningShip(definition)) + { + return new DefaultBehaviorRuntime + { + Kind = LocalAutoMine, + HomeSystemId = systemId, + HomeStationId = null, + AreaSystemId = systemId, + ItemId = "ore", + }; + } + + return new DefaultBehaviorRuntime + { + Kind = HoldPosition, + HomeSystemId = systemId, + HomeStationId = homeStation?.Id, + AreaSystemId = systemId, + WaitSeconds = 4f, + Radius = 24f, + }; + } private static bool HasMeaningfulDelta(WorldDelta delta) => delta.RequiresSnapshotRefresh @@ -367,7 +921,6 @@ public sealed class WorldService || delta.Policies.Count > 0 || delta.Ships.Count > 0 || delta.Factions.Count > 0 - || delta.PlayerFaction is not null || delta.Geopolitics is not null; private void Unsubscribe(Guid subscriberId) @@ -415,7 +968,6 @@ public sealed class WorldService Policies = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Policies : [], Ships = delta.Ships.Where((ship) => systemFilter is null || ship.SystemId == systemFilter).ToList(), Factions = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Factions : [], - PlayerFaction = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.PlayerFaction : null, Geopolitics = string.Equals(scope.ScopeKind, "universe", StringComparison.OrdinalIgnoreCase) ? delta.Geopolitics : null, Scope = scope, }; diff --git a/apps/backend/appsettings.Development.json b/apps/backend/appsettings.Development.json index 0cc03e9..f6d99d5 100644 --- a/apps/backend/appsettings.Development.json +++ b/apps/backend/appsettings.Development.json @@ -5,12 +5,6 @@ "Microsoft.AspNetCore": "Warning" } }, - "WorldGeneration": { - "TargetSystemCount": 2, - "UseKnownSystems": true, - "AiControllerFactionCount": 0, - "GeneratePlayerFaction": false - }, "Balance": { "SimulationSpeedMultiplier": 1.5, "YPlane": 4, @@ -24,5 +18,27 @@ }, "OrbitalSimulation": { "SimulatedSecondsPerRealSecond": 0 + }, + "Auth": { + "ConnectionString": "Host=127.0.0.1;Port=5432;Database=spacegame;Username=spacegame;Password=spacegame", + "DevSeedUsers": [ + { + "Email": "gm", + "Password": "gm", + "Roles": [ "gm" ] + }, + { + "Email": "admin", + "Password": "admin", + "Roles": [ "admin", "gm" ] + } + ] + }, + "Jwt": { + "Issuer": "space-game-dev", + "Audience": "space-game-viewer", + "SigningKey": "space-game-development-signing-key-change-me", + "AccessTokenLifetimeMinutes": 30, + "RefreshTokenLifetimeDays": 30 } } diff --git a/apps/backend/appsettings.json b/apps/backend/appsettings.json index 9f83ed7..04ecae4 100644 --- a/apps/backend/appsettings.json +++ b/apps/backend/appsettings.json @@ -8,10 +8,6 @@ "StaticData": { "DataRoot": "../../shared/data/" }, - "WorldGeneration": { - "TargetSystemCount": 160, - "UseKnownSystems": true - }, "Balance": { "SimulationSpeedMultiplier": 1.5, "YPlane": 4, @@ -26,5 +22,15 @@ "OrbitalSimulation": { "SimulatedSecondsPerRealSecond": 0 }, + "Auth": { + "ConnectionString": "Host=127.0.0.1;Port=5432;Database=spacegame;Username=spacegame;Password=spacegame" + }, + "Jwt": { + "Issuer": "space-game", + "Audience": "space-game-viewer", + "SigningKey": "dev-only-change-me-space-game-signing-key", + "AccessTokenLifetimeMinutes": 30, + "RefreshTokenLifetimeDays": 30 + }, "AllowedHosts": "*" } diff --git a/apps/viewer/src/App.vue b/apps/viewer/src/App.vue index 2a9a570..1325025 100644 --- a/apps/viewer/src/App.vue +++ b/apps/viewer/src/App.vue @@ -1,14 +1,21 @@ diff --git a/apps/viewer/src/ViewerAppController.ts b/apps/viewer/src/ViewerAppController.ts index d7b4cd4..cf21e8f 100644 --- a/apps/viewer/src/ViewerAppController.ts +++ b/apps/viewer/src/ViewerAppController.ts @@ -31,6 +31,9 @@ import { LocalLayer } from "./viewerLocalLayer"; import type { HistoryWindowState, ViewerHudBindings, ViewerHudState } from "./viewerHudState"; import { describeSelectable } from "./viewerSelection"; import { entityIdToSelectable, selectionToEntityId, type ViewerSelectionStore } from "./ui/stores/viewerSelection"; +import { useViewerSceneStore } from "./ui/stores/viewerScene"; +import { useViewerOrderContextMenuStore } from "./ui/stores/viewerOrderContextMenu"; +import { viewerPinia } from "./ui/stores/pinia"; import type { FactionSnapshot } from "./contracts"; import type { CameraMode, @@ -68,6 +71,8 @@ export class ViewerAppController { readonly hudState: ViewerHudState; readonly selectionStore: ViewerSelectionStore; + private readonly sceneStore = useViewerSceneStore(viewerPinia); + private readonly orderContextMenuStore = useViewerOrderContextMenuStore(viewerPinia); private readonly historyLayerEl: HTMLDivElement; private readonly marqueeEl: HTMLDivElement; private readonly hoverLabelEl: HTMLDivElement; @@ -156,6 +161,8 @@ export class ViewerAppController { this.disposeEventBindings(); this.unsubscribeSelectionStore(); this.stream?.close(); + this.sceneStore.reset(); + this.orderContextMenuStore.close(); this.renderSurface.dispose(); disposeSceneResources(this.universeLayer.scene); disposeSceneResources(this.galaxyLayer.scene); @@ -206,6 +213,7 @@ export class ViewerAppController { } private applySelectedItems(items: Selectable[], source: "viewer" | "ui") { + this.orderContextMenuStore.close(); this.selectedItems = items; if (items.length === 1) { const selection = items[0]; @@ -224,6 +232,7 @@ export class ViewerAppController { kind: Selectable["kind"] | null, entityId: string | null, ) { + this.orderContextMenuStore.close(); const selection = entityIdToSelectable(kind, entityId); this.selectedItems = selection ? [selection] : []; this.navigationController.syncFollowStateFromSelection(); @@ -270,6 +279,9 @@ export class ViewerAppController { this.currentDistance = nextState.currentDistance; this.povLevel = nextState.povLevel; this.orbitPitch = nextState.orbitPitch; + if (this.sceneStore.povLevel !== this.povLevel) { + this.sceneStore.setViewContext(this.activeSystemId ?? null, this.povLevel); + } this.navigationController.updateActiveSystem(); if (this.cameraMode === "follow" && this.navigationController.updateFollowCamera(delta)) { diff --git a/apps/viewer/src/api.ts b/apps/viewer/src/api.ts index 2a3feda..6606cc9 100644 --- a/apps/viewer/src/api.ts +++ b/apps/viewer/src/api.ts @@ -2,7 +2,12 @@ import type { WorldDelta, WorldSnapshot } from "./contracts"; import type { TelemetrySnapshot } from "./contractsTelemetry"; import type { BalanceSettings } from "./contractsBalance"; import type { PlayerFactionSnapshot } from "./contractsPlayerFaction"; +import type { AuthSessionResponse, ForgotPasswordResponse } from "./contractsAuth"; +import type { ShipAutomationCatalogSnapshot } from "./contractsShipAutomation"; +import type { FactionSnapshot } from "./contractsFactions"; import type { ShipSnapshot } from "./contractsShips"; +import type { StationSnapshot } from "./contractsInfrastructure"; +import { clearAuthSession, getAuthSession, setAuthSession } from "./authSession"; import type { PlayerAssetAssignmentCommandRequest, PlayerAutomationPolicyCommandRequest, @@ -23,16 +28,54 @@ export interface WorldStreamScope { bubbleId?: string | null; } -async function fetchJson(input: RequestInfo | URL, init?: RequestInit): Promise { - const response = await fetch(input, init); +async function fetchJson(input: RequestInfo | URL, init?: RequestInit, options?: { skipAuth?: boolean; skipRefresh?: boolean }): Promise { + const headers = new Headers(init?.headers); + if (!options?.skipAuth) { + const session = getAuthSession(); + if (session?.accessToken) { + headers.set("Authorization", `Bearer ${session.accessToken}`); + } + } + + const response = await fetch(input, { + ...init, + headers, + }); + if (response.status === 401 && !options?.skipAuth && !options?.skipRefresh) { + const refreshed = await tryRefreshSession(); + if (refreshed) { + return fetchJson(input, init, { skipRefresh: true }); + } + } if (!response.ok) { throw new Error(`${init?.method ?? "GET"} ${typeof input === "string" ? input : input.toString()} failed with ${response.status}`); } return response.json() as Promise; } +async function tryRefreshSession(): Promise { + const session = getAuthSession(); + if (!session?.refreshToken) { + return false; + } + + const response = await fetch("/api/auth/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refreshToken: session.refreshToken }), + }); + if (!response.ok) { + clearAuthSession(); + return false; + } + + const nextSession = await response.json() as AuthSessionResponse; + setAuthSession(nextSession); + return true; +} + export async function fetchWorldSnapshot(signal?: AbortSignal) { - return fetchJson("/api/world", { signal }); + return fetchJson("/api/world", { signal }, { skipAuth: true }); } export function openWorldStream( @@ -86,16 +129,80 @@ export async function updateBalance(settings: BalanceSettings) { }); } +export async function createFaction(request: { factionId: string }) { + return fetchJson("/api/gm/factions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); +} + +export async function spawnShip(request: { factionId: string; systemId: string; shipId?: string | null; behaviorKind?: string | null }) { + return fetchJson("/api/gm/ships", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); +} + +export async function spawnStation(request: { factionId: string; systemId: string; objective?: string | null; label?: string | null }) { + return fetchJson("/api/gm/stations", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); +} + export async function resetWorld() { return fetchJson("/api/world/reset", { method: "POST", }); } +export async function register(request: { email: string; password: string }) { + const session = await fetchJson("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }, { skipAuth: true, skipRefresh: true }); + setAuthSession(session); + return session; +} + +export async function login(request: { email: string; password: string }) { + const session = await fetchJson("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }, { skipAuth: true, skipRefresh: true }); + setAuthSession(session); + return session; +} + +export async function forgotPassword(request: { email: string }) { + return fetchJson("/api/auth/forgot-password", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }, { skipAuth: true, skipRefresh: true }); +} + +export async function resetPassword(request: { token: string; newPassword: string }) { + await fetchJson("/api/auth/reset-password", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }, { skipAuth: true, skipRefresh: true }); +} + export async function fetchPlayerFaction(signal?: AbortSignal) { return fetchJson("/api/player-faction", { signal }); } +export async function fetchShipAutomationCatalog(signal?: AbortSignal) { + return fetchJson("/api/ships/catalog", { signal }, { skipAuth: true }); +} + export async function createPlayerOrganization(request: PlayerOrganizationCommandRequest) { return fetchJson("/api/player-faction/organizations", { method: "POST", @@ -182,3 +289,9 @@ export async function updateShipDefaultBehavior(shipId: string, request: ShipDef body: JSON.stringify(request), }); } + +export async function removeShipOrder(shipId: string, orderId: string) { + return fetchJson(`/api/ships/${shipId}/orders/${orderId}`, { + method: "DELETE", + }); +} diff --git a/apps/viewer/src/assets/backdrop1.webp b/apps/viewer/src/assets/backdrop1.webp new file mode 100644 index 0000000000000000000000000000000000000000..dbb316f0d61a36b5f54e4dcf50a5f60dd5c5a720 GIT binary patch literal 348810 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%0A6)Ia|-hn3(*9=!9;)!(c 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

pTaXyY%sz1gHtCG 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 F006O7(yIUf literal 0 HcmV?d00001 diff --git a/apps/viewer/src/authSession.ts b/apps/viewer/src/authSession.ts new file mode 100644 index 0000000..f7c5f53 --- /dev/null +++ b/apps/viewer/src/authSession.ts @@ -0,0 +1,76 @@ +import type { AuthSessionResponse } from "./contractsAuth"; + +const STORAGE_KEY = "space-game.auth.session"; + +export interface AuthSession { + userId: string; + email: string; + roles: string[]; + accessToken: string; + accessTokenExpiresAtUtc: string; + refreshToken: string; + refreshTokenExpiresAtUtc: string; +} + +let currentSession: AuthSession | null = loadStoredSession(); +const listeners = new Set<(session: AuthSession | null) => void>(); + +export function getAuthSession(): AuthSession | null { + return currentSession; +} + +export function setAuthSession(session: AuthSessionResponse | null) { + currentSession = session ? { ...session } : null; + persistSession(currentSession); + notifyListeners(); +} + +export function clearAuthSession() { + currentSession = null; + persistSession(null); + notifyListeners(); +} + +export function subscribeToAuthSession(listener: (session: AuthSession | null) => void) { + listeners.add(listener); + return () => listeners.delete(listener); +} + +function loadStoredSession(): AuthSession | null { + if (typeof window === "undefined") { + return null; + } + + const raw = window.localStorage.getItem(STORAGE_KEY); + if (!raw) { + return null; + } + + try { + const parsed = JSON.parse(raw) as AuthSession; + return parsed?.accessToken && parsed?.refreshToken + ? { ...parsed, roles: Array.isArray(parsed.roles) ? parsed.roles : [] } + : null; + } catch { + return null; + } +} + +function persistSession(session: AuthSession | null) { + if (typeof window === "undefined") { + return; + } + + if (!session) { + window.localStorage.removeItem(STORAGE_KEY); + return; + } + + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(session)); +} + +function notifyListeners() { + for (const listener of listeners) { + listener(currentSession); + } +} diff --git a/apps/viewer/src/components/AuthLandingPage.vue b/apps/viewer/src/components/AuthLandingPage.vue new file mode 100644 index 0000000..48b739e --- /dev/null +++ b/apps/viewer/src/components/AuthLandingPage.vue @@ -0,0 +1,185 @@ + + + diff --git a/apps/viewer/src/components/AuthSessionPanel.vue b/apps/viewer/src/components/AuthSessionPanel.vue new file mode 100644 index 0000000..ecb9f3f --- /dev/null +++ b/apps/viewer/src/components/AuthSessionPanel.vue @@ -0,0 +1,138 @@ + + + diff --git a/apps/viewer/src/components/ViewerEntityBrowserPanel.vue b/apps/viewer/src/components/ViewerEntityBrowserPanel.vue new file mode 100644 index 0000000..feac357 --- /dev/null +++ b/apps/viewer/src/components/ViewerEntityBrowserPanel.vue @@ -0,0 +1,322 @@ + + + diff --git a/apps/viewer/src/components/ViewerEntityInspectorPanel.vue b/apps/viewer/src/components/ViewerEntityInspectorPanel.vue new file mode 100644 index 0000000..901a6c0 --- /dev/null +++ b/apps/viewer/src/components/ViewerEntityInspectorPanel.vue @@ -0,0 +1,594 @@ + + + diff --git a/apps/viewer/src/components/ViewerShipOrderContextMenu.vue b/apps/viewer/src/components/ViewerShipOrderContextMenu.vue new file mode 100644 index 0000000..a67e9c7 --- /dev/null +++ b/apps/viewer/src/components/ViewerShipOrderContextMenu.vue @@ -0,0 +1,320 @@ + + + diff --git a/apps/viewer/src/components/gm/GmOpsWindow.vue b/apps/viewer/src/components/gm/GmOpsWindow.vue index f296221..8762020 100644 --- a/apps/viewer/src/components/gm/GmOpsWindow.vue +++ b/apps/viewer/src/components/gm/GmOpsWindow.vue @@ -1,5 +1,5 @@