GameBackendResolver.cs
1 using System.Text.Json; 2 using GUNRPG.Application.Backend; 3 using GUNRPG.Infrastructure.Backend; 4 using GUNRPG.Infrastructure.Persistence; 5 using LiteDB; 6 7 namespace GUNRPG.Infrastructure; 8 9 /// <summary> 10 /// Resolves the appropriate game backend (online or offline) based on current state. 11 /// Mode is determined by operator state, not configuration. 12 /// </summary> 13 public sealed class GameBackendResolver 14 { 15 private readonly HttpClient _httpClient; 16 private readonly OfflineStore _offlineStore; 17 private readonly JsonSerializerOptions _jsonOptions; 18 19 public GameBackendResolver(HttpClient httpClient, OfflineStore offlineStore, JsonSerializerOptions? jsonOptions = null) 20 { 21 _httpClient = httpClient; 22 _offlineStore = offlineStore; 23 _jsonOptions = jsonOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); 24 } 25 26 /// <summary> 27 /// The current mode of operation. 28 /// </summary> 29 public GameMode CurrentMode { get; private set; } = GameMode.Online; 30 31 /// <summary> 32 /// Resolves the appropriate backend based on server reachability and local state. 33 /// When the server is reachable and an infiled operator exists, sync is run first. 34 /// Gameplay is blocked if sync fails (chain-of-trust enforcement). 35 /// </summary> 36 public async Task<IGameBackend> ResolveAsync() 37 { 38 var serverReachable = await IsServerReachableAsync(); 39 40 if (serverReachable) 41 { 42 var onlineBackend = new OnlineGameBackend(_httpClient, _offlineStore, _jsonOptions); 43 44 // If an operator was infiled offline, sync must succeed before allowing online play. 45 var activeOp = _offlineStore.GetActiveInfiledOperator(); 46 if (activeOp != null) 47 { 48 Console.WriteLine($"[SYNC] Infiled operator {activeOp.Id} found — synchronizing before returning online."); 49 IExfilSyncService syncService = new ExfilSyncService(_offlineStore, onlineBackend); 50 var syncResult = await syncService.SyncAsync(activeOp.Id); 51 if (!syncResult.Success) 52 { 53 Console.WriteLine($"[SYNC] Sync failed: {syncResult.FailureReason}. Gameplay blocked until operator is re-infiled."); 54 if (syncResult.IsIntegrityFailure) 55 { 56 // Integrity violation is permanent — remove the snapshot so subsequent 57 // calls to ResolveAsync don't loop on the same unresolvable failure. 58 // The operator must re-infil with a clean slate. 59 _offlineStore.RemoveInfiledOperator(activeOp.Id); 60 Console.WriteLine($"[SYNC] Infiled snapshot for operator {activeOp.Id} removed. Re-infil required."); 61 } 62 CurrentMode = GameMode.Blocked; 63 return onlineBackend; 64 } 65 66 Console.WriteLine($"[SYNC] Sync succeeded — {syncResult.EnvelopesSynced} envelope(s) uploaded."); 67 } 68 else 69 { 70 // No infiled operator: log any residual unsynced results from other operators. 71 LogUnsyncedResults(); 72 } 73 74 CurrentMode = GameMode.Online; 75 Console.WriteLine("[MODE] Online mode — server is reachable."); 76 return onlineBackend; 77 } 78 79 if (_offlineStore.HasActiveInfiledOperator()) 80 { 81 CurrentMode = GameMode.Offline; 82 var activeOp = _offlineStore.GetActiveInfiledOperator(); 83 Console.WriteLine($"[MODE] Offline mode — server unreachable, using infiled operator snapshot (ID: {activeOp?.Id})."); 84 LogUnsyncedResults(); 85 return new OfflineGameBackend(_offlineStore); 86 } 87 88 CurrentMode = GameMode.Blocked; 89 Console.WriteLine("[MODE] Blocked — server unreachable and no infiled operator available. Gameplay blocked."); 90 return new OnlineGameBackend(_httpClient, _offlineStore, _jsonOptions); 91 } 92 93 /// <summary> 94 /// Logs the count of unsynced offline mission results. 95 /// </summary> 96 private void LogUnsyncedResults() 97 { 98 var unsyncedResults = _offlineStore.GetAllUnsyncedResults(); 99 if (unsyncedResults.Count > 0) 100 { 101 Console.WriteLine($"[SYNC] {unsyncedResults.Count} unsynced offline mission result(s) pending."); 102 } 103 } 104 105 /// <summary> 106 /// Checks if the API server is reachable. 107 /// </summary> 108 public async Task<bool> IsServerReachableAsync() 109 { 110 try 111 { 112 using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); 113 using var response = await _httpClient.GetAsync( 114 "health", 115 HttpCompletionOption.ResponseHeadersRead, 116 cts.Token); 117 // Any completed HTTP response (regardless of status code) means the server is reachable. 118 return true; 119 } 120 catch (Exception ex) 121 { 122 Console.WriteLine($"[MODE] Server connectivity check failed: {ex.Message}"); 123 return false; 124 } 125 } 126 } 127 128 /// <summary> 129 /// Represents the current game mode. 130 /// </summary> 131 public enum GameMode 132 { 133 /// <summary>Server reachable, full functionality available.</summary> 134 Online, 135 /// <summary>Server unreachable, using infiled operator snapshot.</summary> 136 Offline, 137 /// <summary>Server unreachable, no infiled operator — gameplay blocked.</summary> 138 Blocked 139 }