mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 23:55:43 +00:00
Cache Eagle game state locally (#7007)
This commit is contained in:
@@ -270,6 +270,28 @@ namespace eagle {
|
||||
new ConcurrentDictionary<ShardokGameId, ShardokGameModel>();
|
||||
|
||||
_currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();
|
||||
|
||||
RestoreCachedStateIfAvailable(factionId);
|
||||
}
|
||||
|
||||
private void RestoreCachedStateIfAvailable(FactionId? factionId) {
|
||||
var cachedState = EagleGameStateDiskCache.Load(GameId, factionId);
|
||||
if (cachedState == null) { return; }
|
||||
|
||||
_currentModel.GsView = cachedState.GameStateView;
|
||||
_currentModel.BattalionTypes = cachedState.GameStateView.BattalionTypes.ToDictionary(
|
||||
bt => bt.TypeId,
|
||||
bt => bt);
|
||||
_currentModel.ChronicleEntries = cachedState.GameStateView.ChronicleEntries.ToList();
|
||||
|
||||
lock (_resultCountLock) {
|
||||
_lastUnfilteredResultCount = cachedState.UnfilteredResultCount;
|
||||
}
|
||||
_lastAppliedUnfilteredCount = cachedState.UnfilteredResultCount;
|
||||
|
||||
_connectionLogger.LogLine(
|
||||
$"[STATE_CACHE] Restored game {GameId} at count " +
|
||||
$"{cachedState.UnfilteredResultCount}");
|
||||
}
|
||||
|
||||
private ShardokGameModel MakeGameModel(ShardokGameId shardokGameId) {
|
||||
@@ -479,6 +501,7 @@ namespace eagle {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.BattleProgressResponse:
|
||||
_currentModel.BattleDay = updateItem.BattleProgressResponse.CurrentDay;
|
||||
ApplyBattleProgressUpdate(updateItem.BattleProgressResponse.UpdatedBattles);
|
||||
SaveCurrentStateToCache();
|
||||
if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
|
||||
break;
|
||||
|
||||
@@ -693,6 +716,23 @@ namespace eagle {
|
||||
if (countAfter > _lastAppliedUnfilteredCount) {
|
||||
_lastAppliedUnfilteredCount = countAfter;
|
||||
}
|
||||
SaveCurrentStateToCache(countAfter);
|
||||
}
|
||||
|
||||
private void SaveCurrentStateToCache(int unfilteredResultCount) {
|
||||
if (unfilteredResultCount <= 0 || _currentModel.GsView == null) { return; }
|
||||
|
||||
EagleGameStateDiskCache.Save(
|
||||
GameId,
|
||||
_currentModel.PlayerId,
|
||||
unfilteredResultCount,
|
||||
_currentModel.GsView);
|
||||
}
|
||||
|
||||
private void SaveCurrentStateToCache() {
|
||||
int count;
|
||||
lock (_resultCountLock) { count = _lastUnfilteredResultCount; }
|
||||
SaveCurrentStateToCache(count);
|
||||
}
|
||||
|
||||
private static HashSet<FactionId> InvitationAcceptedFactionIds(
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Google.Protobuf;
|
||||
using Net.Eagle0.Eagle.Views;
|
||||
using UnityEngine;
|
||||
|
||||
namespace eagle {
|
||||
public static class EagleGameStateDiskCache {
|
||||
private const int CurrentSchemaVersion = 1;
|
||||
|
||||
[Serializable]
|
||||
private class CacheFile {
|
||||
public int schemaVersion;
|
||||
public long gameId;
|
||||
public bool hasPlayerId;
|
||||
public int playerId;
|
||||
public int unfilteredResultCount;
|
||||
public string gameStateViewBase64;
|
||||
public string savedAtUtc;
|
||||
}
|
||||
|
||||
public class CachedGameState {
|
||||
public GameStateView GameStateView { get; set; }
|
||||
public int UnfilteredResultCount { get; set; }
|
||||
}
|
||||
|
||||
private static string _cacheDirectory;
|
||||
|
||||
public static CachedGameState Load(long gameId, int? playerId) {
|
||||
EnsureCacheDirectoryInitialized();
|
||||
|
||||
var path = CachePath(gameId, playerId);
|
||||
if (!File.Exists(path)) { return null; }
|
||||
|
||||
try {
|
||||
var cache = JsonUtility.FromJson<CacheFile>(File.ReadAllText(path));
|
||||
if (cache == null || cache.schemaVersion != CurrentSchemaVersion) { return null; }
|
||||
if (cache.gameId != gameId) { return null; }
|
||||
if (cache.hasPlayerId != playerId.HasValue) { return null; }
|
||||
if (cache.hasPlayerId && cache.playerId != playerId.Value) { return null; }
|
||||
if (cache.unfilteredResultCount <= 0) { return null; }
|
||||
if (String.IsNullOrEmpty(cache.gameStateViewBase64)) { return null; }
|
||||
|
||||
var bytes = Convert.FromBase64String(cache.gameStateViewBase64);
|
||||
var gameStateView = GameStateView.Parser.ParseFrom(bytes);
|
||||
if (gameStateView.GameId != gameId) { return null; }
|
||||
|
||||
return new CachedGameState {
|
||||
GameStateView = gameStateView,
|
||||
UnfilteredResultCount = cache.unfilteredResultCount
|
||||
};
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"Failed to load Eagle game state cache for game {gameId}: {e}");
|
||||
TryDelete(path);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void
|
||||
Save(long gameId, int? playerId, int unfilteredResultCount, GameStateView gameStateView) {
|
||||
if (unfilteredResultCount <= 0 || gameStateView == null) { return; }
|
||||
|
||||
EnsureCacheDirectoryInitialized();
|
||||
|
||||
try {
|
||||
Directory.CreateDirectory(CacheDirectory);
|
||||
var cache = new CacheFile {
|
||||
schemaVersion = CurrentSchemaVersion,
|
||||
gameId = gameId,
|
||||
hasPlayerId = playerId.HasValue,
|
||||
playerId = playerId ?? 0,
|
||||
unfilteredResultCount = unfilteredResultCount,
|
||||
gameStateViewBase64 = Convert.ToBase64String(gameStateView.ToByteArray()),
|
||||
savedAtUtc = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
|
||||
File.WriteAllText(CachePath(gameId, playerId), JsonUtility.ToJson(cache));
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"Failed to save Eagle game state cache for game {gameId}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureCacheDirectoryInitialized() {
|
||||
if (_cacheDirectory != null) { return; }
|
||||
|
||||
_cacheDirectory =
|
||||
Path.Combine(Application.persistentDataPath, "eagle0", "eagle_game_state");
|
||||
}
|
||||
|
||||
private static string CacheDirectory => _cacheDirectory;
|
||||
|
||||
private static string CachePath(long gameId, int? playerId) {
|
||||
var playerKey = playerId.HasValue ? playerId.Value.ToString() : "spectator";
|
||||
return Path.Combine(CacheDirectory, $"{gameId:X}-{playerKey}.json");
|
||||
}
|
||||
|
||||
private static void TryDelete(string path) {
|
||||
try {
|
||||
File.Delete(path);
|
||||
} catch (Exception e) {
|
||||
Debug.LogWarning($"Failed to delete corrupt Eagle game state cache {path}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 187990bd7c5c4c02a67ee224d3f6e519
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1095,6 +1095,15 @@ final case class GameController(
|
||||
): GameController =
|
||||
userIdToFactionId.get(userId) match {
|
||||
case Some(fid) =>
|
||||
val safeKnownResultCount =
|
||||
if knownResultCount > fullHistory.count then {
|
||||
SimpleTimedLogger.printLogger.logLine(
|
||||
s"[CLIENT-CACHE] game=$gameId user=$userId reported future result count " +
|
||||
s"$knownResultCount > ${fullHistory.count}; forcing full state refresh"
|
||||
)
|
||||
0
|
||||
} else knownResultCount
|
||||
|
||||
val knownCompleteTextIds = streamingTextStatuses
|
||||
.filter(_.knownComplete)
|
||||
.map(_.llmIdentifier)
|
||||
@@ -1109,7 +1118,7 @@ final case class GameController(
|
||||
factionId = fid,
|
||||
clientId = clientId,
|
||||
grpcObserver = responseObserver,
|
||||
unfilteredKnownHistoryCount = knownResultCount,
|
||||
unfilteredKnownHistoryCount = safeKnownResultCount,
|
||||
knownShardokResultCounts = knownShardokResultCounts,
|
||||
history = fullHistory,
|
||||
availableCommands = GameController.toProtoAvailableCommands(
|
||||
|
||||
Reference in New Issue
Block a user