mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 21:35:42 +00:00
Clear stale selector on command replacement
This commit is contained in:
@@ -380,10 +380,6 @@ jobs:
|
||||
validate_env "EAGLE_IMAGE" "${EAGLE_IMAGE}" "" || VALIDATION_FAILED=1
|
||||
validate_env "JWT_PRIVATE_KEY" "${JWT_PRIVATE_KEY}" "" || VALIDATION_FAILED=1
|
||||
validate_env "DO_REGISTRY_TOKEN" "${DO_REGISTRY_TOKEN}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_HOST" "${EAGLE_POSTGRES_HOST}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_DATABASE" "${EAGLE_POSTGRES_DATABASE}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_USER" "${EAGLE_POSTGRES_USER}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_PASSWORD" "${EAGLE_POSTGRES_PASSWORD}" "" || VALIDATION_FAILED=1
|
||||
|
||||
if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
|
||||
echo ""
|
||||
@@ -431,7 +427,7 @@ jobs:
|
||||
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
export EAGLE_HISTORY_BACKEND="${EAGLE_HISTORY_BACKEND:-postgres}"
|
||||
export EAGLE_HISTORY_BACKEND="${EAGLE_HISTORY_BACKEND:-sqlite}"
|
||||
export EAGLE_POSTGRES_HOST="${EAGLE_POSTGRES_HOST}"
|
||||
export EAGLE_POSTGRES_PORT="${EAGLE_POSTGRES_PORT}"
|
||||
export EAGLE_POSTGRES_DATABASE="${EAGLE_POSTGRES_DATABASE}"
|
||||
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
|
||||
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}"
|
||||
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-sqlite}"
|
||||
EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}"
|
||||
EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}"
|
||||
EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}"
|
||||
@@ -88,7 +88,7 @@ services:
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
|
||||
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}"
|
||||
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-sqlite}"
|
||||
EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}"
|
||||
EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}"
|
||||
EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}"
|
||||
|
||||
@@ -68,7 +68,7 @@ private GameObject GetEffectPrefab(string beastName) {
|
||||
|
||||
### 4. Wire up in the scene
|
||||
|
||||
In `Assets/Scenes/Eagle.unity`, find the `ProvinceBeastsController` component on the Eagle map object and assign your new prefab to the new field.
|
||||
In `Gameplay.unity`, find the `ProvinceBeastsController` component and assign your new prefab to the new field.
|
||||
|
||||
### 5. Test
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ Create a tool to programmatically regenerate the Eagle strategic map images with
|
||||
| `Assets/Eagle/Materials/*.png` | Replaced province masks |
|
||||
| `Assets/Eagle/map_bw_labels.png` | Either: regenerated, or removed if using dynamic labels |
|
||||
| `province_map.tsv` | Add centroid columns |
|
||||
| `Assets/Scenes/Eagle.unity` | Add ProvinceLabelsController, label prefab instances |
|
||||
| `Assets/Gameplay.unity` | Add ProvinceLabelsController, label prefab instances |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ temporary reference snapshot.
|
||||
- `Assets/Scenes/Shared.unity`, `Connection.unity`, `Eagle.unity`, and `Shardok.unity` are loaded additively for their application modes.
|
||||
- `ProjectSettings/EditorBuildSettings.asset` includes the split scenes only.
|
||||
- `Assets/Gameplay.unity` is no longer in build settings, but is still kept in the repo until the split-scene flow has gone through broader build and TestFlight validation.
|
||||
- Editor migration tooling that rebuilt split scenes from `Gameplay.unity` has been removed.
|
||||
- Editor migration tooling still references `Gameplay.unity` while the legacy scene is kept as a source snapshot.
|
||||
|
||||
This means runtime no longer depends on the monolithic scene, but the legacy asset cleanup is
|
||||
intentionally deferred.
|
||||
@@ -150,7 +150,7 @@ Validation:
|
||||
|
||||
- Keep `Gameplay.unity` out of normal startup and build settings.
|
||||
- Keep `Gameplay.unity` in the repo temporarily as a reference snapshot if useful, then delete it in a separate cleanup PR.
|
||||
- Remove transitional helpers whose only purpose was loading, rebuilding, or testing the legacy scene.
|
||||
- Remove transitional helpers whose only purpose was loading or testing the legacy scene.
|
||||
|
||||
Validation:
|
||||
|
||||
@@ -184,15 +184,14 @@ Validation:
|
||||
- Whether `Shared` should always load at startup or only after successful connection.
|
||||
- How long to keep `Gameplay.unity` after the split scene flow is fully validated.
|
||||
|
||||
## Completed Startup Work
|
||||
## Suggested First PR
|
||||
|
||||
The initial startup and scene migration work is complete:
|
||||
The first implementation PR should not move Eagle or Shardok. It should:
|
||||
|
||||
1. `Main.unity` is the startup scene in build settings.
|
||||
2. The split-scene bootstrap path is explicit and logged.
|
||||
3. `Shared` plus `Connection` load by default.
|
||||
4. Eagle and Shardok scene transitions use the split scenes.
|
||||
5. The legacy `Gameplay.unity` fallback has been removed.
|
||||
6. Editor-only scene migration tooling has been removed.
|
||||
1. Make `Main.unity` the startup scene in build settings.
|
||||
2. Make the split-scene bootstrap path explicit and logged.
|
||||
3. Load `Shared` plus `Connection` by default.
|
||||
4. Keep a fallback flag for loading `Gameplay.unity`.
|
||||
5. Validate login/lobby startup in Editor and CI.
|
||||
|
||||
That leaves broader build validation and eventual `Gameplay.unity` deletion as the remaining cleanup work.
|
||||
That gives a controlled base for subsequent subsystem migrations.
|
||||
|
||||
@@ -87,7 +87,7 @@ Create these prefabs in `Assets/Eagle/Effects/`:
|
||||
|
||||
### Scene Setup
|
||||
|
||||
Wire up in `Assets/Scenes/Eagle.unity`:
|
||||
Wire up in `Gameplay.unity`:
|
||||
1. Add controller components to the Map GameObject
|
||||
2. Assign `mapContainer`, `centroidsJson` (shared with other controllers)
|
||||
3. Assign the effect prefab for each controller
|
||||
|
||||
@@ -681,7 +681,10 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException(str);
|
||||
Debug.LogWarning(str);
|
||||
_commandPanelController.SetAvailableCommandAndSelector(null, null);
|
||||
mapController.TargetedProvinces = new List<ProvinceId>();
|
||||
commandButtonPanelController.ClearButtonContents();
|
||||
}
|
||||
|
||||
var firstCommand = Model.AvailableCommandsByProvince.First().Value.Commands.First();
|
||||
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using common.GUIUtils;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public static class ConnectionSceneMigrationTool {
|
||||
private const string GameplayScenePath = "Assets/Gameplay.unity";
|
||||
private const string ConnectionScenePath = "Assets/Scenes/Connection.unity";
|
||||
private const string EagleScenePath = "Assets/Scenes/Eagle.unity";
|
||||
private const string ShardokScenePath = "Assets/Scenes/Shardok.unity";
|
||||
private const string SharedScenePath = "Assets/Scenes/Shared.unity";
|
||||
private const string ConnectionCanvasName = "ConnectionCanvas";
|
||||
private const string EagleCanvasName = "Eagle Canvas";
|
||||
private const string EaglePlaceholderCanvasName = "EagleCanvas";
|
||||
private const string ErrorCanvasName = "ErrorCanvas";
|
||||
private const string PersistentCanvasName = "PersistentCanvas";
|
||||
private const string ShardokContainerName = "Shardok Container";
|
||||
private const string ShardokPlaceholderCanvasName = "ShardokCanvas";
|
||||
private const string CopiedSceneIncompleteReason =
|
||||
"Connection scene was copied from Gameplay but still needs cross-scene reference " +
|
||||
"migration before it can replace Gameplay.";
|
||||
|
||||
[MenuItem("Eagle0/Scenes/Rebuild Connection Scene From Gameplay")]
|
||||
public static void RebuildConnectionSceneFromGameplay() {
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
|
||||
Debug.Log("Connection scene rebuild canceled because modified scenes were not saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EditorUtility.DisplayDialog(
|
||||
"Rebuild Connection Scene",
|
||||
"This will replace the ConnectionCanvas in Assets/Scenes/Connection.unity " +
|
||||
"with a copy from Assets/Gameplay.unity. The copied canvas will stay " +
|
||||
"inactive and marked not production-ready.",
|
||||
"Rebuild",
|
||||
"Cancel")) {
|
||||
Debug.Log("Connection scene rebuild canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
|
||||
var sourceConnectionCanvas = FindRootByName(gameplayScene, ConnectionCanvasName);
|
||||
if (sourceConnectionCanvas == null) {
|
||||
Debug.LogError(
|
||||
$"Could not find root GameObject '{ConnectionCanvasName}' in {GameplayScenePath}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionScene =
|
||||
EditorSceneManager.OpenScene(ConnectionScenePath, OpenSceneMode.Additive);
|
||||
foreach (var existingCanvas in FindRootsByName(connectionScene, ConnectionCanvasName)) {
|
||||
Object.DestroyImmediate(existingCanvas);
|
||||
}
|
||||
|
||||
var copiedCanvas = Object.Instantiate(sourceConnectionCanvas);
|
||||
copiedCanvas.name = ConnectionCanvasName;
|
||||
copiedCanvas.SetActive(false);
|
||||
SceneManager.MoveGameObjectToScene(copiedCanvas, connectionScene);
|
||||
|
||||
MarkSceneNotReady(copiedCanvas);
|
||||
LogConnectionHandlerReferenceReport(copiedCanvas);
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(connectionScene);
|
||||
EditorSceneManager.SaveScene(connectionScene);
|
||||
EditorSceneManager.OpenScene(ConnectionScenePath, OpenSceneMode.Single);
|
||||
|
||||
Debug.Log(
|
||||
$"Rebuilt {ConnectionScenePath} from {GameplayScenePath}. " +
|
||||
$"{ConnectionCanvasName} is inactive and productionReady is false.");
|
||||
}
|
||||
|
||||
[MenuItem("Eagle0/Scenes/Rebuild Eagle And Shardok Scenes From Gameplay")]
|
||||
public static void RebuildEagleAndShardokScenesFromGameplay() {
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
|
||||
Debug.Log(
|
||||
"Eagle/Shardok scene rebuild canceled because modified scenes were not saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EditorUtility.DisplayDialog(
|
||||
"Rebuild Eagle And Shardok Scenes",
|
||||
"This will replace the placeholder content in Assets/Scenes/Eagle.unity and " +
|
||||
"Assets/Scenes/Shardok.unity with copies from Assets/Gameplay.unity. " +
|
||||
"References that belong to other split scenes will be cleared so they " +
|
||||
"can be wired at runtime.",
|
||||
"Rebuild",
|
||||
"Cancel")) {
|
||||
Debug.Log("Eagle/Shardok scene rebuild canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
|
||||
|
||||
var copiedEagleCanvas = RebuildSceneObjectFromGameplay(
|
||||
gameplayScene,
|
||||
EagleScenePath,
|
||||
EagleCanvasName,
|
||||
EagleCanvasName,
|
||||
EaglePlaceholderCanvasName,
|
||||
EagleCanvasName);
|
||||
if (copiedEagleCanvas == null) return;
|
||||
|
||||
ClearEagleSceneExternalReferences(copiedEagleCanvas);
|
||||
int clearedEagleReferences =
|
||||
ClearExternalSceneReferences(copiedEagleCanvas, EagleScenePath);
|
||||
var eagleScene = SceneManager.GetSceneByPath(EagleScenePath);
|
||||
EditorSceneManager.MarkSceneDirty(eagleScene);
|
||||
EditorSceneManager.SaveScene(eagleScene);
|
||||
|
||||
var copiedShardokContainer = RebuildSceneObjectFromGameplay(
|
||||
gameplayScene,
|
||||
ShardokScenePath,
|
||||
ShardokContainerName,
|
||||
ShardokContainerName,
|
||||
ShardokPlaceholderCanvasName,
|
||||
ShardokContainerName);
|
||||
if (copiedShardokContainer == null) return;
|
||||
|
||||
ClearShardokSceneExternalReferences(copiedShardokContainer);
|
||||
int clearedShardokReferences =
|
||||
ClearExternalSceneReferences(copiedShardokContainer, ShardokScenePath);
|
||||
var shardokScene = SceneManager.GetSceneByPath(ShardokScenePath);
|
||||
EditorSceneManager.MarkSceneDirty(shardokScene);
|
||||
EditorSceneManager.SaveScene(shardokScene);
|
||||
|
||||
EditorSceneManager.OpenScene(EagleScenePath, OpenSceneMode.Single);
|
||||
|
||||
Debug.Log(
|
||||
$"Rebuilt {EagleScenePath} and {ShardokScenePath} from {GameplayScenePath}. " +
|
||||
"EagleGameController.errorHandler, EagleGameController.shardokContainer, and " +
|
||||
"ShardokGameController.eagleCanvas were cleared for runtime scene wiring. " +
|
||||
$"Cleared {clearedEagleReferences} additional Eagle scene references and " +
|
||||
$"{clearedShardokReferences} additional Shardok scene references.");
|
||||
}
|
||||
|
||||
[MenuItem("Eagle0/Scenes/Rebuild Shared Error UI From Gameplay")]
|
||||
public static void RebuildSharedErrorUIFromGameplay() {
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
|
||||
Debug.Log("Shared error UI rebuild canceled because modified scenes were not saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EditorUtility.DisplayDialog(
|
||||
"Rebuild Shared Error UI",
|
||||
"This will replace the ErrorCanvas in Assets/Scenes/Shared.unity with a copy " +
|
||||
"from Assets/Gameplay.unity.",
|
||||
"Rebuild",
|
||||
"Cancel")) {
|
||||
Debug.Log("Shared error UI rebuild canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
|
||||
var sourceErrorCanvas = FindRootByName(gameplayScene, ErrorCanvasName);
|
||||
if (sourceErrorCanvas == null) {
|
||||
Debug.LogError(
|
||||
$"Could not find root GameObject '{ErrorCanvasName}' in {GameplayScenePath}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var sharedScene = EditorSceneManager.OpenScene(SharedScenePath, OpenSceneMode.Additive);
|
||||
foreach (var existingErrorCanvas in FindRootsByName(sharedScene, ErrorCanvasName)) {
|
||||
Object.DestroyImmediate(existingErrorCanvas);
|
||||
}
|
||||
|
||||
var copiedErrorCanvas = Object.Instantiate(sourceErrorCanvas);
|
||||
copiedErrorCanvas.name = ErrorCanvasName;
|
||||
ClearSharedErrorUIExternalReferences(copiedErrorCanvas);
|
||||
SceneManager.MoveGameObjectToScene(copiedErrorCanvas, sharedScene);
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(sharedScene);
|
||||
EditorSceneManager.SaveScene(sharedScene);
|
||||
EditorSceneManager.OpenScene(SharedScenePath, OpenSceneMode.Single);
|
||||
|
||||
Debug.Log($"Rebuilt {SharedScenePath} {ErrorCanvasName} from {GameplayScenePath}.");
|
||||
}
|
||||
|
||||
[MenuItem("Eagle0/Scenes/Rebuild Shared Persistent UI From Gameplay")]
|
||||
public static void RebuildSharedPersistentUIFromGameplay() {
|
||||
if (!Application.isBatchMode &&
|
||||
!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
|
||||
Debug.Log(
|
||||
"Shared persistent UI rebuild canceled because modified scenes were not saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Application.isBatchMode &&
|
||||
!EditorUtility.DisplayDialog(
|
||||
"Rebuild Shared Persistent UI",
|
||||
"This will replace the PersistentCanvas in Assets/Scenes/Shared.unity " +
|
||||
"with a copy from Assets/Gameplay.unity. Cross-scene references will " +
|
||||
"be cleared so they can be wired at runtime.",
|
||||
"Rebuild",
|
||||
"Cancel")) {
|
||||
Debug.Log("Shared persistent UI rebuild canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
|
||||
var sourcePersistentCanvas = FindRootByName(gameplayScene, PersistentCanvasName);
|
||||
if (sourcePersistentCanvas == null) {
|
||||
Debug.LogError(
|
||||
$"Could not find root GameObject '{PersistentCanvasName}' in " +
|
||||
$"{GameplayScenePath}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var sharedScene = EditorSceneManager.OpenScene(SharedScenePath, OpenSceneMode.Additive);
|
||||
foreach (var existingPersistentCanvas in FindRootsByName(
|
||||
sharedScene,
|
||||
PersistentCanvasName)) {
|
||||
Object.DestroyImmediate(existingPersistentCanvas);
|
||||
}
|
||||
|
||||
var copiedPersistentCanvas = Object.Instantiate(sourcePersistentCanvas);
|
||||
copiedPersistentCanvas.name = PersistentCanvasName;
|
||||
SceneManager.MoveGameObjectToScene(copiedPersistentCanvas, sharedScene);
|
||||
|
||||
int clearedReferences =
|
||||
ClearExternalSceneReferences(copiedPersistentCanvas, SharedScenePath);
|
||||
|
||||
EditorSceneManager.MarkSceneDirty(sharedScene);
|
||||
EditorSceneManager.SaveScene(sharedScene);
|
||||
EditorSceneManager.OpenScene(SharedScenePath, OpenSceneMode.Single);
|
||||
|
||||
Debug.Log(
|
||||
$"Rebuilt {SharedScenePath} {PersistentCanvasName} from {GameplayScenePath}. " +
|
||||
$"Cleared {clearedReferences} cross-scene references for runtime wiring.");
|
||||
}
|
||||
|
||||
private static GameObject RebuildSceneObjectFromGameplay(
|
||||
Scene gameplayScene,
|
||||
string targetScenePath,
|
||||
string sourceObjectName,
|
||||
string copiedObjectName,
|
||||
params string[] objectNamesToReplace) {
|
||||
var sourceObject = FindObjectByName(gameplayScene, sourceObjectName);
|
||||
if (sourceObject == null) {
|
||||
Debug.LogError(
|
||||
$"Could not find GameObject '{sourceObjectName}' in {GameplayScenePath}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var targetScene = EditorSceneManager.OpenScene(targetScenePath, OpenSceneMode.Additive);
|
||||
foreach (var objectNameToReplace in objectNamesToReplace) {
|
||||
var existingObjects = FindObjectsByName(targetScene, objectNameToReplace).ToList();
|
||||
foreach (var existingObject in existingObjects) {
|
||||
Object.DestroyImmediate(existingObject);
|
||||
}
|
||||
}
|
||||
|
||||
var copiedObject = Object.Instantiate(sourceObject);
|
||||
copiedObject.name = copiedObjectName;
|
||||
SceneManager.MoveGameObjectToScene(copiedObject, targetScene);
|
||||
return copiedObject;
|
||||
}
|
||||
|
||||
private static void ClearEagleSceneExternalReferences(GameObject eagleCanvas) {
|
||||
var eagleController = eagleCanvas.GetComponent<eagle.EagleGameController>();
|
||||
if (eagleController == null) return;
|
||||
|
||||
eagleController.errorHandler = null;
|
||||
eagleController.shardokContainer = null;
|
||||
EditorUtility.SetDirty(eagleController);
|
||||
}
|
||||
|
||||
private static void ClearShardokSceneExternalReferences(GameObject shardokContainer) {
|
||||
var shardokController = shardokContainer.GetComponent<Shardok.ShardokGameController>();
|
||||
if (shardokController == null) return;
|
||||
|
||||
shardokController.eagleCanvas = null;
|
||||
EditorUtility.SetDirty(shardokController);
|
||||
}
|
||||
|
||||
private static int ClearExternalSceneReferences(GameObject sceneRoot, string targetScenePath) {
|
||||
int clearedReferences = 0;
|
||||
foreach (var component in sceneRoot.GetComponentsInChildren<Component>(
|
||||
includeInactive: true)) {
|
||||
if (component == null) continue;
|
||||
|
||||
var serializedObject = new SerializedObject(component);
|
||||
var property = serializedObject.GetIterator();
|
||||
bool modified = false;
|
||||
while (property.NextVisible(enterChildren: true)) {
|
||||
if (property.propertyType != SerializedPropertyType.ObjectReference) continue;
|
||||
if (property.objectReferenceValue == null) continue;
|
||||
if (!ReferencesDifferentScene(property.objectReferenceValue, targetScenePath))
|
||||
continue;
|
||||
|
||||
property.objectReferenceValue = null;
|
||||
clearedReferences++;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(component);
|
||||
}
|
||||
}
|
||||
|
||||
return clearedReferences;
|
||||
}
|
||||
|
||||
private static bool ReferencesDifferentScene(Object referencedObject, string targetScenePath) {
|
||||
var referencedGameObject = GetReferencedGameObject(referencedObject);
|
||||
if (referencedGameObject == null) return false;
|
||||
|
||||
var referencedScene = referencedGameObject.scene;
|
||||
return referencedScene.IsValid() && !string.IsNullOrEmpty(referencedScene.path) &&
|
||||
referencedScene.path != targetScenePath;
|
||||
}
|
||||
|
||||
private static void ClearSharedErrorUIExternalReferences(GameObject errorCanvas) {
|
||||
var errorHandler = errorCanvas.GetComponent<ErrorHandler>();
|
||||
if (errorHandler == null) { return; }
|
||||
|
||||
errorHandler.bugReportPanel = null;
|
||||
EditorUtility.SetDirty(errorHandler);
|
||||
}
|
||||
|
||||
[MenuItem("Eagle0/Scenes/Validate Connection Scene Readiness")]
|
||||
public static void ValidateConnectionSceneReadiness() {
|
||||
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
|
||||
Debug.Log(
|
||||
"Connection scene validation canceled because modified scenes were not saved.");
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionScene =
|
||||
EditorSceneManager.OpenScene(ConnectionScenePath, OpenSceneMode.Single);
|
||||
var connectionCanvases = FindRootsByName(connectionScene, ConnectionCanvasName).ToList();
|
||||
if (connectionCanvases.Count != 1) {
|
||||
Debug.LogError(
|
||||
$"{ConnectionScenePath} should contain exactly one root named " +
|
||||
$"{ConnectionCanvasName}, but found {connectionCanvases.Count}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var connectionCanvas = connectionCanvases[0];
|
||||
var readiness = connectionCanvas.GetComponent<ConnectionSceneReadiness>();
|
||||
if (readiness == null) {
|
||||
Debug.LogError($"{ConnectionCanvasName} is missing ConnectionSceneReadiness.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (readiness.IsReady(out var reason)) {
|
||||
var handler = connectionCanvas.GetComponentInChildren<ConnectionHandler>(
|
||||
includeInactive: true);
|
||||
if (handler == null) {
|
||||
Debug.LogError(
|
||||
$"{ConnectionScenePath} is marked ready but {ConnectionCanvasName} " +
|
||||
"does not contain a ConnectionHandler.");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"{ConnectionScenePath} is marked production-ready.");
|
||||
LogConnectionHandlerReferenceReport(connectionCanvas);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionCanvas.activeSelf) {
|
||||
Debug.LogWarning(
|
||||
$"{ConnectionCanvasName} is active while the scene is not production-ready. " +
|
||||
"Bootstrap will still fall back, but the scene should stay inert until migration " +
|
||||
"is complete.");
|
||||
}
|
||||
|
||||
Debug.Log($"{ConnectionScenePath} is not production-ready: {reason}");
|
||||
LogConnectionHandlerReferenceReport(connectionCanvas);
|
||||
}
|
||||
|
||||
private static GameObject FindRootByName(Scene scene, string rootName) {
|
||||
return FindRootsByName(scene, rootName).FirstOrDefault();
|
||||
}
|
||||
|
||||
private static IEnumerable<GameObject> FindRootsByName(Scene scene, string rootName) {
|
||||
return scene.GetRootGameObjects().Where(root => root.name == rootName);
|
||||
}
|
||||
|
||||
private static GameObject FindObjectByName(Scene scene, string objectName) {
|
||||
return FindObjectsByName(scene, objectName).FirstOrDefault();
|
||||
}
|
||||
|
||||
private static IEnumerable<GameObject> FindObjectsByName(Scene scene, string objectName) {
|
||||
return scene.GetRootGameObjects()
|
||||
.SelectMany(root => root.GetComponentsInChildren<Transform>(includeInactive: true))
|
||||
.Select(transform => transform.gameObject)
|
||||
.Where(gameObject => gameObject.name == objectName);
|
||||
}
|
||||
|
||||
private static void MarkSceneNotReady(GameObject connectionCanvas) {
|
||||
var readiness = connectionCanvas.GetComponent<ConnectionSceneReadiness>();
|
||||
if (readiness == null)
|
||||
readiness = connectionCanvas.AddComponent<ConnectionSceneReadiness>();
|
||||
|
||||
var serializedReadiness = new SerializedObject(readiness);
|
||||
serializedReadiness.FindProperty("productionReady").boolValue = false;
|
||||
serializedReadiness.FindProperty("incompleteReason").stringValue =
|
||||
CopiedSceneIncompleteReason;
|
||||
serializedReadiness.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
private static void LogConnectionHandlerReferenceReport(GameObject connectionCanvas) {
|
||||
var handler =
|
||||
connectionCanvas.GetComponentInChildren<ConnectionHandler>(includeInactive: true);
|
||||
if (handler == null) {
|
||||
Debug.LogWarning($"{ConnectionCanvasName} does not contain a ConnectionHandler yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
LogReferenceSceneWarning(handler.eagleCanvas, nameof(handler.eagleCanvas));
|
||||
LogReferenceSceneWarning(handler.shardokContainer, nameof(handler.shardokContainer));
|
||||
LogReferenceSceneWarning(handler.errorHandler, nameof(handler.errorHandler));
|
||||
}
|
||||
|
||||
private static void LogReferenceSceneWarning(Object referencedObject, string fieldName) {
|
||||
if (referencedObject == null) {
|
||||
Debug.LogWarning($"ConnectionHandler.{fieldName} is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
var referencedGameObject = GetReferencedGameObject(referencedObject);
|
||||
if (referencedGameObject == null) return;
|
||||
|
||||
var scene = referencedGameObject.scene;
|
||||
if (scene.IsValid() && scene.path != ConnectionScenePath) {
|
||||
Debug.LogWarning(
|
||||
$"ConnectionHandler.{fieldName} still references '{referencedGameObject.name}' " +
|
||||
$"from {scene.path}. Move or replace this dependency before marking " +
|
||||
"Connection production-ready.");
|
||||
}
|
||||
}
|
||||
|
||||
private static GameObject GetReferencedGameObject(Object referencedObject) {
|
||||
if (referencedObject is GameObject gameObject) return gameObject;
|
||||
if (referencedObject is Component component) return component.gameObject;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d07c0b7fb8a4e45ac80c1479562dd9a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -7,14 +7,13 @@
|
||||
- `Connection.unity`, `Eagle.unity`, `Shardok.unity`, and `Shared.unity` are included in build settings.
|
||||
- `Gameplay.unity` remains in the repo as a reference snapshot, but is no longer a normal runtime fallback.
|
||||
- Runtime scene references are wired by `GameSceneManager` after additive scene loads.
|
||||
- Editor-only migration tooling that regenerated split scenes from `Gameplay.unity` has been removed.
|
||||
- Obsolete migration planning docs and unused scene-loading test scripts have been removed.
|
||||
|
||||
## Remaining Cleanup
|
||||
|
||||
1. Keep playtesting the split-scene flow through connection, Eagle, custom battles, Shardok battles, and return-to-lobby.
|
||||
2. Delete `Gameplay.unity` in a separate cleanup PR once builds and TestFlight have verified the split scenes.
|
||||
3. Remove any remaining legacy-scene documentation after the legacy scene is deleted.
|
||||
2. Remove editor-only migration tooling after we are confident no more objects need to be regenerated from `Gameplay.unity`.
|
||||
3. Delete `Gameplay.unity` in a separate cleanup PR once builds and TestFlight have verified the split scenes.
|
||||
4. Remove stale migration planning docs after the legacy scene is deleted.
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# Scene Separation Plan
|
||||
|
||||
## Current Situation
|
||||
- Single massive Gameplay.unity scene (7.5MB, 255k lines)
|
||||
- Contains ALL game UI, connection screens, battle UI, etc.
|
||||
- High risk of corruption when editing
|
||||
|
||||
## Proposed Multi-Scene Architecture
|
||||
|
||||
### Core Scenes (Always Loaded)
|
||||
1. **Main.unity** - Minimal bootstrap scene
|
||||
- Scene manager
|
||||
- Persistent game systems
|
||||
- Audio manager
|
||||
- Network connection manager
|
||||
|
||||
### Loadable Scenes
|
||||
2. **Connection.unity** - Login and lobby UI
|
||||
- ConnectionCanvas
|
||||
- Connection Panel
|
||||
- Game Selection Panel
|
||||
- Custom Battle Panel
|
||||
|
||||
3. **Eagle.unity** - Strategic game UI
|
||||
- Eagle Canvas
|
||||
- Map Canvas
|
||||
- Command Canvas
|
||||
- Heroes and Battalions UI
|
||||
|
||||
4. **Shardok.unity** - Tactical battle UI
|
||||
- Shardok Canvas
|
||||
- Battle grid
|
||||
- Unit placement UI
|
||||
- Combat UI
|
||||
|
||||
5. **Shared.unity** - Common UI elements
|
||||
- Error Canvas
|
||||
- Popup Canvas
|
||||
- Confirmation dialogs
|
||||
- Overlay canvases
|
||||
|
||||
## Scene Loading Strategy
|
||||
|
||||
```csharp
|
||||
// Example scene loading flow
|
||||
Main.unity (always loaded)
|
||||
↓
|
||||
Connection.unity (additive load)
|
||||
↓ (on game select)
|
||||
Unload Connection.unity
|
||||
Load Eagle.unity + Shared.unity (additive)
|
||||
↓ (on battle start)
|
||||
Load Shardok.unity (additive)
|
||||
↓ (on battle end)
|
||||
Unload Shardok.unity
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Create Scene Structure
|
||||
1. Create new empty scenes
|
||||
2. Set up basic scene loading system
|
||||
3. Test scene transitions work
|
||||
|
||||
### Phase 2: Move Non-Critical Elements
|
||||
1. Start with Shared UI elements (popups, errors)
|
||||
2. These are least likely to break core gameplay
|
||||
3. Test thoroughly
|
||||
|
||||
### Phase 3: Move Connection UI
|
||||
1. Connection/lobby UI is self-contained
|
||||
2. Only needs to communicate via ConnectionHandler
|
||||
3. Natural separation point
|
||||
|
||||
### Phase 4: Separate Eagle and Shardok
|
||||
1. These are already somewhat separated
|
||||
2. Different canvases for each mode
|
||||
3. Can be loaded/unloaded as needed
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Reduced Complexity**: Each scene is manageable size
|
||||
2. **Parallel Development**: Multiple developers can work on different scenes
|
||||
3. **Memory Efficiency**: Only load what's needed
|
||||
4. **Faster Load Times**: Smaller scenes load faster
|
||||
5. **Version Control**: Fewer merge conflicts
|
||||
6. **Easier Debugging**: Isolate issues to specific scenes
|
||||
|
||||
## Safe Migration Process
|
||||
|
||||
### Step 1: Create SceneManager
|
||||
```csharp
|
||||
public class GameSceneManager : MonoBehaviour
|
||||
{
|
||||
public static GameSceneManager Instance;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadSceneAdditive(string sceneName)
|
||||
{
|
||||
await SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
|
||||
}
|
||||
|
||||
public async Task UnloadScene(string sceneName)
|
||||
{
|
||||
await SceneManager.UnloadSceneAsync(sceneName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Test with Empty Scenes First
|
||||
1. Create Main.unity with just SceneManager
|
||||
2. Create empty Connection.unity
|
||||
3. Test loading Connection additively
|
||||
4. Test unloading Connection
|
||||
5. Verify no crashes or issues
|
||||
|
||||
### Step 3: Move One Component at a Time
|
||||
1. Move ErrorCanvas to Shared.unity
|
||||
2. Test it still works
|
||||
3. Move next component
|
||||
4. Repeat until all moved
|
||||
|
||||
## Scene Settings
|
||||
|
||||
Each scene should have:
|
||||
- Appropriate lighting settings
|
||||
- Required cameras (if any)
|
||||
- Event systems (only one active)
|
||||
- Canvas scalers configured
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| References break between scenes | Use ScriptableObjects or singleton patterns |
|
||||
| UI events don't work | Ensure EventSystem is present |
|
||||
| Performance overhead | Profile scene loading, use async |
|
||||
| Missing dependencies | Document all cross-scene dependencies |
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Main.unity loads without errors
|
||||
- [ ] Can load Connection.unity additively
|
||||
- [ ] Can transition from Connection to Eagle
|
||||
- [ ] Can start battle (load Shardok)
|
||||
- [ ] Can end battle (unload Shardok)
|
||||
- [ ] All UI elements visible and functional
|
||||
- [ ] No memory leaks from scene loading/unloading
|
||||
- [ ] Build succeeds with multi-scene setup
|
||||
|
||||
This approach is MUCH safer than trying to extract prefabs from a massive scene file!
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e0e3b2b2183b4e50bdb154c5a3c9b29
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,86 @@
|
||||
# Scene Separation Implementation Guide
|
||||
|
||||
## Current Status
|
||||
|
||||
I've created the basic infrastructure for separating the massive Gameplay.unity into multiple scenes:
|
||||
|
||||
### Created Files:
|
||||
1. **Main.unity** - Minimal bootstrap scene with SceneManager
|
||||
2. **Connection.unity** - Empty scene for connection/lobby UI
|
||||
3. **GameSceneManager.cs** - Scene loading/unloading manager
|
||||
4. **SceneLoadTester.cs** - Test script to verify scene loading works
|
||||
|
||||
## Next Steps
|
||||
|
||||
### 1. Test Basic Scene Loading (Do This First!)
|
||||
1. Open Unity
|
||||
2. Open `Assets/Scenes/Main.unity`
|
||||
3. Add the SceneLoadTester component to the SceneManager GameObject (optional, for testing)
|
||||
4. Add both scenes to Build Settings:
|
||||
- File → Build Settings
|
||||
- Add Open Scenes (Main.unity)
|
||||
- Add `Assets/Scenes/Connection.unity`
|
||||
5. Play the scene and check console for any errors
|
||||
|
||||
### 2. If Scene Loading Works:
|
||||
Create the remaining empty scenes:
|
||||
- Eagle.unity
|
||||
- Shardok.unity
|
||||
- Shared.unity
|
||||
|
||||
### 3. Start Moving UI Components
|
||||
|
||||
**Safest Order:**
|
||||
1. **ErrorCanvas** → Shared.unity (least dependencies)
|
||||
2. **PopupCanvas** → Shared.unity
|
||||
3. **ConnectionCanvas + panels** → Connection.unity
|
||||
4. **Eagle UI** → Eagle.unity
|
||||
5. **Shardok UI** → Shardok.unity
|
||||
|
||||
### 4. How to Move Components Safely
|
||||
|
||||
For each component:
|
||||
1. In Gameplay.unity, find the GameObject
|
||||
2. Right-click → Copy
|
||||
3. Open target scene (e.g., Connection.unity)
|
||||
4. Right-click in Hierarchy → Paste
|
||||
5. Save the target scene
|
||||
6. Test that everything still works
|
||||
7. Only then delete from Gameplay.unity
|
||||
|
||||
### 5. Update ConnectionHandler
|
||||
|
||||
Once Connection UI is moved, update ConnectionHandler to work with scene loading:
|
||||
|
||||
```csharp
|
||||
// Instead of:
|
||||
connectionCanvas.gameObject.SetActive(true);
|
||||
|
||||
// Use:
|
||||
await GameSceneManager.Instance.TransitionToConnection();
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Always test after each move** - Don't move multiple components at once
|
||||
2. **Keep Gameplay.unity as backup** - Don't delete it until everything works
|
||||
3. **Check references** - Some scripts may reference GameObjects across scenes
|
||||
4. **One EventSystem** - Ensure only one EventSystem is active across all loaded scenes
|
||||
|
||||
## Benefits Once Complete
|
||||
|
||||
- Faster Unity startup (smaller scenes)
|
||||
- Easier collaboration (less merge conflicts)
|
||||
- Better performance (load only what's needed)
|
||||
- Cleaner organization
|
||||
- Easier to debug specific UI sections
|
||||
|
||||
## If Something Goes Wrong
|
||||
|
||||
1. Check Build Settings - all scenes must be added
|
||||
2. Check for missing references in Inspector
|
||||
3. Ensure EventSystem exists in one of the loaded scenes
|
||||
4. Check console for specific error messages
|
||||
5. Can always revert to using original Gameplay.unity
|
||||
|
||||
This approach is MUCH safer than trying to edit the 7.5MB scene file directly!
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a28b46a2ab5484cb5a6ea85229d2885d
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,89 @@
|
||||
# Safe Scene Refactoring Plan
|
||||
|
||||
## Problem
|
||||
The Gameplay.unity file is massive (255k lines, 7.5MB) making it:
|
||||
- Prone to crashes when edited
|
||||
- Difficult to maintain and review
|
||||
- Risk of corruption when multiple developers work on it
|
||||
|
||||
## Safe Solution Strategy
|
||||
|
||||
Instead of directly editing the massive scene file, we'll use a gradual approach:
|
||||
|
||||
### Phase 1: Create Empty Prefab Structures (SAFE)
|
||||
1. Create new, minimal prefab versions of UI components
|
||||
2. These will be empty initially and won't affect the game
|
||||
3. Test that Unity can load and build successfully
|
||||
|
||||
### Phase 2: Runtime Loading System (SAFE)
|
||||
1. Create a script that can instantiate prefabs at runtime
|
||||
2. Test this system in isolation first
|
||||
3. Gradually migrate components one by one
|
||||
|
||||
### Phase 3: Component Migration (SAFE)
|
||||
1. Start with the simplest components first
|
||||
2. Use A/B testing - keep old version as fallback
|
||||
3. Migrate one component at a time with thorough testing
|
||||
|
||||
## Phase 1 Implementation
|
||||
|
||||
### Step 1.1: Create UI Directory Structure
|
||||
```
|
||||
Assets/UI/
|
||||
├── Prefabs/
|
||||
│ ├── Connection/
|
||||
│ │ ├── ConnectionPanel.prefab
|
||||
│ │ ├── GameSelectionPanel.prefab
|
||||
│ │ └── CustomBattlePanel.prefab
|
||||
│ ├── Game/
|
||||
│ │ ├── CommandCanvas.prefab
|
||||
│ │ ├── PopupCanvas.prefab
|
||||
│ │ └── ReservesCanvas.prefab
|
||||
│ └── Shared/
|
||||
│ ├── ErrorCanvas.prefab
|
||||
│ └── ConfirmationCanvas.prefab
|
||||
└── Scripts/
|
||||
├── PrefabLoader.cs
|
||||
└── UIManager.cs
|
||||
```
|
||||
|
||||
### Step 1.2: Create Empty Prefabs
|
||||
- Start with ConnectionPanel.prefab (simplest connection UI)
|
||||
- Create minimal structure with required components
|
||||
- Test Unity can load without errors
|
||||
|
||||
### Step 1.3: Runtime Loading Test
|
||||
- Create PrefabLoader script that can instantiate prefabs
|
||||
- Test loading prefabs at runtime
|
||||
- Ensure no conflicts with existing UI
|
||||
|
||||
## Benefits of This Approach
|
||||
|
||||
1. **Zero Risk**: Won't affect existing functionality
|
||||
2. **Incremental**: Can be done component by component
|
||||
3. **Testable**: Each step can be verified independently
|
||||
4. **Reversible**: Can rollback any step if needed
|
||||
5. **Collaborative**: Multiple developers can work safely
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. ConnectionPanel (smallest, least complex)
|
||||
2. CustomBattlePanel (isolated functionality)
|
||||
3. GameSelectionPanel (moderate complexity)
|
||||
4. Command-related canvases (high complexity)
|
||||
5. Game UI canvases (highest complexity)
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- After each component: Unity loads without errors
|
||||
- After each component: Game functionality unchanged
|
||||
- After each component: Build succeeds
|
||||
- Before final migration: Comprehensive gameplay testing
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
- Keep original Gameplay.unity as Gameplay_backup.unity
|
||||
- Each prefab has a feature flag for enable/disable
|
||||
- Can instantly revert to original scene if issues arise
|
||||
|
||||
This approach ensures we never break the working game while gradually improving the scene structure.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b097cd2b606a540029692ab16a1fc29a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Test script to verify scene loading works correctly.
|
||||
/// This should be attached to a GameObject in Main.unity for testing.
|
||||
/// </summary>
|
||||
public class SceneLoadTester : MonoBehaviour {
|
||||
void Start() {
|
||||
Debug.Log("SceneLoadTester: Starting scene load test...");
|
||||
StartCoroutine(TestSceneLoading());
|
||||
}
|
||||
|
||||
IEnumerator TestSceneLoading() {
|
||||
// Wait for SceneManager to initialize
|
||||
yield return new WaitForSeconds(1f);
|
||||
|
||||
if (GameSceneManager.Instance == null) {
|
||||
Debug.LogError("SceneLoadTester: GameSceneManager.Instance is null!");
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.Log("SceneLoadTester: Testing Connection scene load...");
|
||||
|
||||
// Test loading Connection scene
|
||||
var loadTask = GameSceneManager.Instance.LoadSceneAdditive("Connection");
|
||||
yield return new WaitUntil(() => loadTask.IsCompleted);
|
||||
|
||||
if (loadTask.IsFaulted) {
|
||||
Debug.LogError(
|
||||
$"SceneLoadTester: Failed to load Connection scene: {loadTask.Exception}");
|
||||
} else {
|
||||
Debug.Log("SceneLoadTester: Connection scene loaded successfully!");
|
||||
}
|
||||
|
||||
// Wait a bit
|
||||
yield return new WaitForSeconds(2f);
|
||||
|
||||
// Test unloading Connection scene
|
||||
Debug.Log("SceneLoadTester: Testing Connection scene unload...");
|
||||
var unloadTask = GameSceneManager.Instance.UnloadScene("Connection");
|
||||
yield return new WaitUntil(() => unloadTask.IsCompleted);
|
||||
|
||||
if (unloadTask.IsFaulted) {
|
||||
Debug.LogError(
|
||||
$"SceneLoadTester: Failed to unload Connection scene: {unloadTask.Exception}");
|
||||
} else {
|
||||
Debug.Log("SceneLoadTester: Connection scene unloaded successfully!");
|
||||
}
|
||||
|
||||
Debug.Log("SceneLoadTester: Test complete!");
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3d8f9175cb2b4c4d929c4ee2e8f7058
|
||||
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Simple test script to verify SimpleErrorHandler is working.
|
||||
/// Add this to any GameObject and it will test error display after 2 seconds.
|
||||
/// </summary>
|
||||
public class TestErrorHandler : MonoBehaviour {
|
||||
void Start() {
|
||||
// Test the error handler after 2 seconds
|
||||
Invoke(nameof(TestError), 2f);
|
||||
}
|
||||
|
||||
void TestError() {
|
||||
Debug.LogError("Test error from TestErrorHandler - this should show in the error panel!");
|
||||
|
||||
// Also test the static method
|
||||
Invoke(nameof(TestStaticError), 3f);
|
||||
}
|
||||
|
||||
void TestStaticError() {
|
||||
SimpleErrorHandler.ShowErrorStatic(
|
||||
"Test static error message",
|
||||
"This is a test stack trace");
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99f0d60b862f64cbcae6307870bce0c8
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unity Scene Analyzer - Safely analyze the Gameplay.unity file structure
|
||||
to identify components that can be extracted into prefabs.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
class UnityObject:
|
||||
def __init__(self, file_id: str, object_type: str):
|
||||
self.file_id = file_id
|
||||
self.object_type = object_type
|
||||
self.name = ""
|
||||
self.components = []
|
||||
self.children = []
|
||||
self.parent = None
|
||||
|
||||
def __repr__(self):
|
||||
return f"UnityObject({self.file_id}, {self.object_type}, '{self.name}')"
|
||||
|
||||
def parse_unity_scene(file_path: str) -> Dict[str, UnityObject]:
|
||||
"""Parse a Unity scene file and extract object hierarchy."""
|
||||
objects = {}
|
||||
current_object = None
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i].strip()
|
||||
|
||||
# Look for object definitions
|
||||
obj_match = re.match(r'^--- !u!(\d+) &(\d+)$', line)
|
||||
if obj_match:
|
||||
object_type = obj_match.group(1)
|
||||
file_id = obj_match.group(2)
|
||||
current_object = UnityObject(file_id, object_type)
|
||||
objects[file_id] = current_object
|
||||
|
||||
# Look for GameObject names (object_type == "1")
|
||||
elif current_object and current_object.object_type == "1" and line.startswith('m_Name:'):
|
||||
name_match = re.match(r'm_Name:\s*(.*)$', line)
|
||||
if name_match:
|
||||
current_object.name = name_match.group(1).strip()
|
||||
|
||||
# Look for component references
|
||||
elif current_object and current_object.object_type == "1" and line.startswith('- component:'):
|
||||
comp_match = re.match(r'- component: \{fileID: (\d+)\}', line)
|
||||
if comp_match:
|
||||
current_object.components.append(comp_match.group(1))
|
||||
|
||||
# Look for RectTransform children
|
||||
elif line.startswith('m_Children:'):
|
||||
i += 1
|
||||
while i < len(lines) and lines[i].startswith(' -'):
|
||||
child_match = re.match(r' - \{fileID: (\d+)\}', lines[i].strip())
|
||||
if child_match and current_object:
|
||||
child_id = child_match.group(1)
|
||||
current_object.children.append(child_id)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
i += 1
|
||||
|
||||
return objects
|
||||
|
||||
def find_canvas_hierarchy(objects: Dict[str, UnityObject]) -> List[Tuple[str, UnityObject]]:
|
||||
"""Find all Canvas objects and their immediate children."""
|
||||
canvases = []
|
||||
|
||||
for obj_id, obj in objects.items():
|
||||
if obj.name and ("Canvas" in obj.name or obj.name in [
|
||||
"ConnectionCanvas", "Command Canvas", "Popup Canvas",
|
||||
"Grid Canvas", "Reserves Canvas", "Unit Info Canvas",
|
||||
"Command Buttons Canvas", "Dominion View Canvas (wide)"
|
||||
]):
|
||||
canvases.append((obj_id, obj))
|
||||
|
||||
return canvases
|
||||
|
||||
def analyze_scene_structure(file_path: str):
|
||||
"""Analyze the Unity scene and suggest prefab extraction."""
|
||||
print(f"Analyzing Unity scene: {file_path}")
|
||||
|
||||
objects = parse_unity_scene(file_path)
|
||||
print(f"Found {len(objects)} Unity objects")
|
||||
|
||||
canvases = find_canvas_hierarchy(objects)
|
||||
print(f"\nFound {len(canvases)} Canvas objects:")
|
||||
|
||||
for canvas_id, canvas_obj in canvases:
|
||||
print(f"\n📋 Canvas: {canvas_obj.name} (ID: {canvas_id})")
|
||||
print(f" Components: {len(canvas_obj.components)}")
|
||||
print(f" Children: {len(canvas_obj.children)}")
|
||||
|
||||
# Show immediate children
|
||||
if canvas_obj.children:
|
||||
print(" Immediate children:")
|
||||
for child_id in canvas_obj.children[:5]: # Show first 5
|
||||
if child_id in objects:
|
||||
child = objects[child_id]
|
||||
print(f" - {child.name or 'Unnamed'} (ID: {child_id})")
|
||||
if len(canvas_obj.children) > 5:
|
||||
print(f" ... and {len(canvas_obj.children) - 5} more")
|
||||
|
||||
# Identify good candidates for prefab extraction
|
||||
print(f"\n🎯 Recommended for prefab extraction:")
|
||||
|
||||
good_candidates = []
|
||||
for canvas_id, canvas_obj in canvases:
|
||||
# Canvas objects with reasonable size and clear purpose
|
||||
if canvas_obj.name in [
|
||||
"ConnectionCanvas", "Command Canvas", "Popup Canvas",
|
||||
"Reserves Canvas", "Command Buttons Canvas"
|
||||
] and len(canvas_obj.children) < 50:
|
||||
good_candidates.append((canvas_id, canvas_obj))
|
||||
print(f" ✅ {canvas_obj.name} - Manageable size ({len(canvas_obj.children)} children)")
|
||||
|
||||
return good_candidates
|
||||
|
||||
if __name__ == "__main__":
|
||||
scene_path = "/Users/dancrosby/CodingProjects/github/eagle0/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Gameplay.unity"
|
||||
analyze_scene_structure(scene_path)
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4b2b2bb94e5a471e86597b8838642c9
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unity Prefab Extractor - Safely extract a GameObject hierarchy from Gameplay.unity
|
||||
and create a standalone prefab file.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from typing import Dict, List, Set, Optional
|
||||
|
||||
def find_gameobject_by_id(file_path: str, target_id: str) -> Optional[Dict]:
|
||||
"""Find a GameObject by its file ID and return its full definition."""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i].strip()
|
||||
|
||||
# Look for the specific GameObject
|
||||
obj_match = re.match(r'^--- !u!1 &' + target_id + r'$', line)
|
||||
if obj_match:
|
||||
# Found the GameObject, now extract its full definition
|
||||
obj_start = i
|
||||
obj_data = {'line_start': obj_start, 'content': []}
|
||||
|
||||
# Read until next object or EOF
|
||||
i += 1
|
||||
while i < len(lines):
|
||||
next_line = lines[i]
|
||||
if next_line.startswith('--- !u!'):
|
||||
break
|
||||
obj_data['content'].append(next_line)
|
||||
i += 1
|
||||
|
||||
obj_data['line_end'] = i
|
||||
return obj_data
|
||||
i += 1
|
||||
|
||||
return None
|
||||
|
||||
def extract_gameobject_info(file_path: str, target_id: str):
|
||||
"""Extract basic information about a GameObject."""
|
||||
obj_data = find_gameobject_by_id(file_path, target_id)
|
||||
if not obj_data:
|
||||
print(f"GameObject with ID {target_id} not found!")
|
||||
return
|
||||
|
||||
name = "Unknown"
|
||||
components = []
|
||||
children = []
|
||||
|
||||
for line in obj_data['content']:
|
||||
line = line.strip()
|
||||
|
||||
# Extract name
|
||||
name_match = re.match(r'm_Name:\s*(.*)$', line)
|
||||
if name_match:
|
||||
name = name_match.group(1).strip()
|
||||
|
||||
# Extract components
|
||||
comp_match = re.match(r'- component: \{fileID: (\d+)\}', line)
|
||||
if comp_match:
|
||||
components.append(comp_match.group(1))
|
||||
|
||||
print(f"GameObject: {name} (ID: {target_id})")
|
||||
print(f" Lines: {obj_data['line_start']} - {obj_data['line_end']}")
|
||||
print(f" Components: {len(components)}")
|
||||
print(f" Component IDs: {components}")
|
||||
|
||||
return {
|
||||
'id': target_id,
|
||||
'name': name,
|
||||
'line_start': obj_data['line_start'],
|
||||
'line_end': obj_data['line_end'],
|
||||
'components': components
|
||||
}
|
||||
|
||||
def main():
|
||||
scene_path = "/Users/dancrosby/CodingProjects/github/eagle0/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Gameplay.unity"
|
||||
|
||||
# The three panels from ConnectionHandler
|
||||
panel_ids = {
|
||||
'connectionPanel': '596196018',
|
||||
'gameSelectionPanel': '1102342355',
|
||||
'customBattlePanel': '1633635477'
|
||||
}
|
||||
|
||||
print("Analyzing Connection UI Panels:")
|
||||
print("=" * 50)
|
||||
|
||||
for panel_name, panel_id in panel_ids.items():
|
||||
print(f"\n{panel_name}:")
|
||||
panel_info = extract_gameobject_info(scene_path, panel_id)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26ecb4dca2c97457c93b1e27c54ccddd
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,94 @@
|
||||
# Eagle0 Lobby UI Improvements Status
|
||||
|
||||
## Overview
|
||||
This document tracks the progress of implementing lobby UI improvements for Eagle0. Due to Unity crashes when attempting to edit the massive 7.5MB Gameplay.unity scene file directly, we've pivoted to creating a new multi-scene architecture that can be developed alongside the existing system.
|
||||
|
||||
## Current Status (2025-07-11)
|
||||
|
||||
### Completed Work
|
||||
|
||||
1. **Multi-Scene Architecture Created**
|
||||
- Created new scene structure: Main.unity (bootstrap), Connection.unity, Eagle.unity, Shardok.unity, and Shared.unity
|
||||
- All scenes are properly configured and can be loaded additively
|
||||
- Main.unity serves as the entry point with BootstrapManager controlling which system to use
|
||||
|
||||
2. **Scene Management System**
|
||||
- Created `GameSceneManager.cs` - singleton that handles scene loading/unloading with async/await
|
||||
- Created `BootstrapManager.cs` - controls whether to use original Gameplay.unity or new scene system
|
||||
- Default configuration uses original Gameplay.unity (safe for merging)
|
||||
|
||||
3. **Error Handling System**
|
||||
- Created `SimpleErrorHandler.cs` - lightweight error handler without MainQueue dependency
|
||||
- Successfully captures Debug.LogError, LogWarning, and LogException
|
||||
- UI components added to Shared.unity scene (Canvas, ErrorPanel, ErrorText, DismissButton)
|
||||
- Tested and working correctly with auto-dismiss functionality
|
||||
|
||||
4. **Testing Infrastructure**
|
||||
- Created `TestErrorHandler.cs` for verifying error handling functionality
|
||||
- BootstrapManager includes scene loading tests (`testSceneLoading` flag)
|
||||
- All scenes load and unload correctly
|
||||
|
||||
### Safe to Merge
|
||||
The current implementation is **safe to merge** because:
|
||||
- Default behavior loads original Gameplay.unity (no changes to existing workflow)
|
||||
- New scene system is opt-in via BootstrapManager settings
|
||||
- All new code is additive - no modifications to existing systems
|
||||
- Can switch between old and new systems by changing a boolean flag
|
||||
|
||||
### How to Switch Between Systems
|
||||
|
||||
In Main.unity scene, find the BootstrapManager component on Bootstrap GameObject:
|
||||
- `useOriginalGameplayScene = true` - Uses existing Gameplay.unity (default)
|
||||
- `useOriginalGameplayScene = false, useNewSceneSystem = true` - Uses new multi-scene system
|
||||
- `testSceneLoading = true` - Runs scene loading tests
|
||||
|
||||
### Next Steps (When Resuming)
|
||||
|
||||
1. **Create Connection UI Components**
|
||||
- Design and implement the connection/lobby UI in Connection.unity
|
||||
- This will be a clean implementation without the complexity of Gameplay.unity
|
||||
|
||||
2. **Migrate Existing UI**
|
||||
- Gradually move UI components from Gameplay.unity to appropriate new scenes
|
||||
- Connection UI → Connection.unity
|
||||
- Game UI → Eagle.unity
|
||||
- Battle UI → Shardok.unity
|
||||
- Shared UI (errors, settings) → Shared.unity
|
||||
|
||||
3. **Scene Transition Logic**
|
||||
- Implement proper transitions from Connection → Eagle when game starts
|
||||
- Handle Eagle ↔ Shardok transitions for battles
|
||||
|
||||
4. **Optimize and Clean Up**
|
||||
- Once new system is stable, can deprecate Gameplay.unity
|
||||
- Remove redundant components and optimize scene loading
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### File Locations
|
||||
- Scene files: `/Assets/Scenes/`
|
||||
- Scripts: `/Assets/UI/Scripts/`
|
||||
- Main entry point: Main.unity with BootstrapManager
|
||||
|
||||
### Key Components
|
||||
- `BootstrapManager.cs` - Controls which scene system to use
|
||||
- `GameSceneManager.cs` - Handles scene loading/unloading
|
||||
- `SimpleErrorHandler.cs` - Error display without external dependencies
|
||||
- `TestErrorHandler.cs` - Testing utility for error handling
|
||||
|
||||
### Known Issues
|
||||
- None currently - system is stable in both modes
|
||||
|
||||
## Git Commit Message Suggestion
|
||||
```
|
||||
feat: Add multi-scene architecture foundation for lobby UI improvements
|
||||
|
||||
- Create new scene structure (Main, Connection, Eagle, Shardok, Shared)
|
||||
- Add scene management system with async loading/unloading
|
||||
- Implement SimpleErrorHandler for new scene system
|
||||
- Default to original Gameplay.unity (opt-in new system)
|
||||
- All changes are additive and safe to merge
|
||||
|
||||
This provides a clean foundation for implementing lobby UI improvements
|
||||
without modifying the existing 7.5MB Gameplay.unity file.
|
||||
```
|
||||
@@ -83,31 +83,6 @@ scala_library(
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "postgres_client_text_store",
|
||||
srcs = ["PostgresClientTextStore.scala"],
|
||||
visibility = [
|
||||
"//src/main/scala/net/eagle0/eagle:__subpackages__",
|
||||
"//src/test/scala/net/eagle0/eagle:__subpackages__",
|
||||
],
|
||||
runtime_deps = [
|
||||
"@maven//:org_postgresql_postgresql",
|
||||
],
|
||||
deps = [
|
||||
":client_text",
|
||||
":client_text_store",
|
||||
":pregenerated_text_store",
|
||||
":text_generation_result",
|
||||
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
|
||||
"//src/main/scala/net/eagle0/common:jfr_events",
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
|
||||
"//src/main/scala/net/eagle0/eagle/model/proto_converters:generated_text_request_converter",
|
||||
],
|
||||
)
|
||||
|
||||
scala_library(
|
||||
name = "sqlite_client_text_store",
|
||||
srcs = ["SqliteClientTextStore.scala"],
|
||||
|
||||
@@ -1,828 +0,0 @@
|
||||
package net.eagle0.eagle.client_text
|
||||
|
||||
import java.sql.{Connection, DriverManager, ResultSet}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
import net.eagle0.common.JfrEvents
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId, GameId}
|
||||
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
|
||||
import net.eagle0.eagle.model.proto_converters.GeneratedTextRequestConverter
|
||||
|
||||
class PostgresClientTextStore private[client_text] (
|
||||
private[client_text] val connection: Connection,
|
||||
gameId: GameId,
|
||||
pregenerated: PregeneratedClientTextStore
|
||||
) extends ClientTextStore {
|
||||
|
||||
private val databaseName = s"postgres-text:${gameId.toHexString}"
|
||||
|
||||
override def completeTexts: Map[ClientTextId, CompleteClientText] = {
|
||||
val sql = "SELECT id, text, requested_after_history_count FROM client_texts WHERE status = 'complete'"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.completeTexts", sql) { event =>
|
||||
try {
|
||||
val rs = stmt.executeQuery()
|
||||
val result = scala.collection.mutable.Map[ClientTextId, CompleteClientText]()
|
||||
while rs.next() do {
|
||||
val id = rs.getString("id")
|
||||
result(id) = CompleteClientText(
|
||||
id = id,
|
||||
text = rs.getString("text"),
|
||||
requestedAfterHistoryCount = rs.getInt("requested_after_history_count")
|
||||
)
|
||||
}
|
||||
event.rows = result.size
|
||||
result.toMap
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
override def incompleteTexts: Map[ClientTextId, IncompleteClientText] = {
|
||||
val sql =
|
||||
"SELECT id, text, llm_request, requested_after_history_count, requested_at_millis, last_update_at_millis FROM client_texts WHERE status = 'incomplete'"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.incompleteTexts", sql) { event =>
|
||||
try {
|
||||
val rs = stmt.executeQuery()
|
||||
val result = scala.collection.mutable.Map[ClientTextId, IncompleteClientText]()
|
||||
while rs.next() do {
|
||||
val id = rs.getString("id")
|
||||
val llmRequest = llmRequestFromResultSet(rs)
|
||||
result(id) = IncompleteClientText(
|
||||
id = id,
|
||||
partialText = rs.getString("text"),
|
||||
llmRequest = llmRequest,
|
||||
requestedAfterHistoryCount = rs.getInt("requested_after_history_count"),
|
||||
requestedAtMillis = rs.getLong("requested_at_millis"),
|
||||
lastUpdateAtMillis = rs.getLong("last_update_at_millis")
|
||||
)
|
||||
}
|
||||
event.rows = result.size
|
||||
result.toMap
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
override def unrequestedTexts: Map[ClientTextId, UnrequestedClientText] = {
|
||||
val sql = "SELECT id, llm_request, requested_after_history_count FROM client_texts WHERE status = 'unrequested'"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.unrequestedTexts", sql) { event =>
|
||||
try {
|
||||
val rs = stmt.executeQuery()
|
||||
val result = scala.collection.mutable.Map[ClientTextId, UnrequestedClientText]()
|
||||
while rs.next() do {
|
||||
val id = rs.getString("id")
|
||||
result(id) = UnrequestedClientText(
|
||||
id = id,
|
||||
llmRequest = llmRequestFromResultSet(rs),
|
||||
requestedAfterHistoryCount = rs.getInt("requested_after_history_count")
|
||||
)
|
||||
}
|
||||
event.rows = result.size
|
||||
result.toMap
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
override def accessibleTo: Map[ClientTextId, Vector[FactionId]] = {
|
||||
val sql = "SELECT text_id, faction_id FROM client_text_visibility ORDER BY text_id, faction_id"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.accessibleTo", sql) { event =>
|
||||
try {
|
||||
val rs = stmt.executeQuery()
|
||||
val result = scala.collection.mutable.Map[ClientTextId, Vector[FactionId]]()
|
||||
var rows = 0
|
||||
while rs.next() do {
|
||||
val textId = rs.getString("text_id")
|
||||
val factionId = rs.getInt("faction_id")
|
||||
result(textId) = result.getOrElse(textId, Vector.empty) :+ factionId
|
||||
rows += 1
|
||||
}
|
||||
event.rows = rows
|
||||
result.toMap
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
override def accessibleToForId(id: ClientTextId): Vector[FactionId] = {
|
||||
val sql = "SELECT faction_id FROM client_text_visibility WHERE text_id = ? ORDER BY faction_id"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.accessibleToForId", sql) { event =>
|
||||
try {
|
||||
stmt.setString(1, id)
|
||||
val rs = stmt.executeQuery()
|
||||
val result = scala.collection.mutable.ArrayBuffer[FactionId]()
|
||||
while rs.next() do result += rs.getInt("faction_id")
|
||||
event.rows = result.size
|
||||
result.toVector
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
override def beginTransaction(): Unit = {
|
||||
if !connection.getAutoCommit then return
|
||||
connection.setAutoCommit(false)
|
||||
}
|
||||
|
||||
override def commitTransaction(): Unit = {
|
||||
if connection.getAutoCommit then return
|
||||
connection.commit()
|
||||
connection.setAutoCommit(true)
|
||||
}
|
||||
|
||||
override def rollbackTransaction(): Unit = {
|
||||
if connection.getAutoCommit then return
|
||||
connection.rollback()
|
||||
connection.setAutoCommit(true)
|
||||
}
|
||||
|
||||
override def saved: ClientTextStore = this
|
||||
|
||||
override def withAddedTextRequest(
|
||||
id: ClientTextId,
|
||||
accessibleTo: Vector[FactionId],
|
||||
llmRequest: GeneratedTextRequestT,
|
||||
requestedAfterHistoryCount: Int
|
||||
): ClientTextStore = {
|
||||
internalRequire(
|
||||
pregenerated.getText(id).isEmpty,
|
||||
s"Text with id $id is pregenerated"
|
||||
)
|
||||
|
||||
if textExists(id) then {
|
||||
println(s"Text with id $id already exists")
|
||||
return this
|
||||
}
|
||||
|
||||
val sql =
|
||||
"""INSERT INTO client_texts (id, status, text, llm_request, requested_after_history_count)
|
||||
|VALUES (?, 'unrequested', '', ?, ?)""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.withAddedTextRequest.insertText", sql) { event =>
|
||||
try {
|
||||
stmt.setString(1, id)
|
||||
stmt.setBytes(2, GeneratedTextRequestConverter.toProto(llmRequest).toByteArray)
|
||||
stmt.setInt(3, requestedAfterHistoryCount)
|
||||
event.rows = stmt.executeUpdate()
|
||||
} finally stmt.close()
|
||||
}
|
||||
|
||||
insertVisibility(id, accessibleTo)
|
||||
this
|
||||
}
|
||||
|
||||
override def getText(id: ClientTextId): TextGenerationResult =
|
||||
pregenerated
|
||||
.getText(id)
|
||||
.map(text => TextGenerationSuccess(text))
|
||||
.getOrElse {
|
||||
val sql = "SELECT status, text FROM client_texts WHERE id = ?"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.getText", sql) { event =>
|
||||
try {
|
||||
stmt.setString(1, id)
|
||||
val rs = stmt.executeQuery()
|
||||
if rs.next() then {
|
||||
event.rows = 1
|
||||
val status = rs.getString("status")
|
||||
val text = rs.getString("text")
|
||||
status match {
|
||||
case "complete" => TextGenerationSuccess(text)
|
||||
case "incomplete" => TextGenerationDependencyInProgress(id, text)
|
||||
case "unrequested" => TextGenerationDependencyWaiting(id)
|
||||
}
|
||||
} else TextGenerationDependencyUnknown(id)
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
override def withMarkedRequested(id: ClientTextId): ClientTextStore = {
|
||||
val now = System.currentTimeMillis()
|
||||
val sql =
|
||||
"""UPDATE client_texts SET status = 'incomplete', requested_at_millis = ?, last_update_at_millis = ?
|
||||
|WHERE id = ? AND status = 'unrequested'""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.withMarkedRequested", sql) { event =>
|
||||
try {
|
||||
stmt.setLong(1, now)
|
||||
stmt.setLong(2, now)
|
||||
stmt.setString(3, id)
|
||||
val updated = stmt.executeUpdate()
|
||||
event.rows = updated
|
||||
if updated == 0 then {
|
||||
throw new EagleInternalException(
|
||||
s"Unrequested text with id $id not found"
|
||||
)
|
||||
}
|
||||
} finally stmt.close()
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
override def withBypassed(id: ClientTextId): ClientTextStore = {
|
||||
val sql = "DELETE FROM client_texts WHERE id = ? AND status = 'unrequested'"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.withBypassed", sql) { event =>
|
||||
try {
|
||||
stmt.setString(1, id)
|
||||
event.rows = stmt.executeUpdate()
|
||||
} finally stmt.close()
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
override def withAppendedText(
|
||||
id: ClientTextId,
|
||||
newText: String,
|
||||
complete: Boolean
|
||||
): ClientTextStoreWithUpdate = {
|
||||
internalRequire(
|
||||
pregenerated.getText(id).isEmpty,
|
||||
s"Text with id $id is pregenerated"
|
||||
)
|
||||
|
||||
val (currentText, llmRequest, requestedAfterHistoryCount, requestedAtMillis) =
|
||||
selectIncompleteText(id)
|
||||
val now = System.currentTimeMillis()
|
||||
val updatedText = currentText + newText
|
||||
val newStatus = if complete then "complete" else "incomplete"
|
||||
|
||||
val sql =
|
||||
"""UPDATE client_texts SET text = ?, status = ?, last_update_at_millis = ?
|
||||
|WHERE id = ?""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.withAppendedText", sql) { event =>
|
||||
try {
|
||||
stmt.setString(1, updatedText)
|
||||
stmt.setString(2, newStatus)
|
||||
stmt.setLong(3, now)
|
||||
stmt.setString(4, id)
|
||||
event.rows = stmt.executeUpdate()
|
||||
} finally stmt.close()
|
||||
}
|
||||
|
||||
val updatedClientText =
|
||||
if complete then
|
||||
CompleteClientText(
|
||||
id = id,
|
||||
text = updatedText,
|
||||
requestedAfterHistoryCount = requestedAfterHistoryCount
|
||||
)
|
||||
else
|
||||
IncompleteClientText(
|
||||
id = id,
|
||||
partialText = updatedText,
|
||||
llmRequest = llmRequest,
|
||||
requestedAfterHistoryCount = requestedAfterHistoryCount,
|
||||
requestedAtMillis = requestedAtMillis,
|
||||
lastUpdateAtMillis = now
|
||||
)
|
||||
|
||||
ClientTextStoreWithUpdate(this, updatedClientText)
|
||||
}
|
||||
|
||||
override def withExtendedVisibility(
|
||||
id: ClientTextId,
|
||||
addedFactionIds: Vector[FactionId]
|
||||
): ClientTextStore = {
|
||||
insertVisibility(id, addedFactionIds)
|
||||
this
|
||||
}
|
||||
|
||||
override def withMovedBackToUnrequested(id: ClientTextId): ClientTextStore = {
|
||||
val status = statusFor(id)
|
||||
if status.isEmpty then {
|
||||
println(s"Warning: Attempted to move non-existent text $id back to unrequested")
|
||||
return this
|
||||
}
|
||||
if !status.contains("incomplete") then {
|
||||
println(s"Warning: Attempted to move non-incomplete text $id back to unrequested")
|
||||
return this
|
||||
}
|
||||
|
||||
val sql =
|
||||
"""UPDATE client_texts SET status = 'unrequested', text = '', requested_at_millis = NULL, last_update_at_millis = NULL
|
||||
|WHERE id = ? AND status = 'incomplete'""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.withMovedBackToUnrequested", sql) { event =>
|
||||
try {
|
||||
stmt.setString(1, id)
|
||||
event.rows = stmt.executeUpdate()
|
||||
} finally stmt.close()
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
override def withAddedCompleteText(
|
||||
id: ClientTextId,
|
||||
text: String,
|
||||
accessibleTo: Vector[FactionId]
|
||||
): ClientTextStore = {
|
||||
if textExists(id) then return this
|
||||
|
||||
val sql =
|
||||
"""INSERT INTO client_texts (id, status, text, requested_after_history_count)
|
||||
|VALUES (?, 'complete', ?, 0)""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.withAddedCompleteText", sql) { event =>
|
||||
try {
|
||||
stmt.setString(1, id)
|
||||
stmt.setString(2, text)
|
||||
event.rows = stmt.executeUpdate()
|
||||
} finally stmt.close()
|
||||
}
|
||||
insertVisibility(id, accessibleTo)
|
||||
this
|
||||
}
|
||||
|
||||
override def withAddedCompleteTexts(
|
||||
texts: Vector[(ClientTextId, String)],
|
||||
accessibleTo: Vector[FactionId]
|
||||
): ClientTextStore = {
|
||||
if texts.isEmpty then return this
|
||||
|
||||
val originalAutoCommit = connection.getAutoCommit
|
||||
connection.setAutoCommit(false)
|
||||
try {
|
||||
val existingIds = readExistingIds()
|
||||
val newTexts = texts.filterNot { case (id, _) => existingIds.contains(id) }
|
||||
if newTexts.isEmpty then {
|
||||
connection.setAutoCommit(originalAutoCommit)
|
||||
return this
|
||||
}
|
||||
|
||||
val insertSql =
|
||||
"""INSERT INTO client_texts (id, status, text, requested_after_history_count)
|
||||
|VALUES (?, 'complete', ?, 0)""".stripMargin
|
||||
val insertStmt = connection.prepareStatement(insertSql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.withAddedCompleteTexts.insertTexts", insertSql) {
|
||||
event =>
|
||||
try {
|
||||
newTexts.foreach {
|
||||
case (id, text) =>
|
||||
insertStmt.setString(1, id)
|
||||
insertStmt.setString(2, text)
|
||||
insertStmt.addBatch()
|
||||
}
|
||||
event.rows = insertStmt.executeBatch().sum
|
||||
} finally insertStmt.close()
|
||||
}
|
||||
|
||||
insertVisibilityBatch(newTexts.map(_._1), accessibleTo)
|
||||
connection.commit()
|
||||
this
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
connection.rollback()
|
||||
throw e
|
||||
} finally connection.setAutoCommit(originalAutoCommit)
|
||||
end try
|
||||
}
|
||||
|
||||
override def withCachedUnrequestedGameState(
|
||||
id: ClientTextId,
|
||||
updatedEntry: UnrequestedClientText
|
||||
): ClientTextStore = this
|
||||
|
||||
override def withCachedIncompleteGameState(
|
||||
id: ClientTextId,
|
||||
updatedEntry: IncompleteClientText
|
||||
): ClientTextStore = this
|
||||
|
||||
override def hasIncompleteTextsAccessibleTo(factionId: FactionId): Boolean = {
|
||||
val sql =
|
||||
"""SELECT EXISTS(
|
||||
| SELECT 1 FROM client_texts t
|
||||
| LEFT JOIN client_text_visibility v ON t.id = v.text_id
|
||||
| WHERE t.status IN ('incomplete', 'unrequested')
|
||||
| AND (v.faction_id IS NULL OR v.faction_id = ?)
|
||||
|)""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.hasIncompleteTextsAccessibleTo", sql) { event =>
|
||||
try {
|
||||
stmt.setInt(1, factionId)
|
||||
val rs = stmt.executeQuery()
|
||||
val result = rs.next() && rs.getBoolean(1)
|
||||
event.rows = if result then 1 else 0
|
||||
result
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
override def getCompleteTextsAccessibleTo(factionId: FactionId): Vector[CompleteClientText] = {
|
||||
val sql =
|
||||
"""SELECT t.id, t.text, t.requested_after_history_count
|
||||
|FROM client_texts t
|
||||
|WHERE t.status = 'complete'
|
||||
|AND (
|
||||
| NOT EXISTS (SELECT 1 FROM client_text_visibility v WHERE v.text_id = t.id)
|
||||
| OR EXISTS (SELECT 1 FROM client_text_visibility v WHERE v.text_id = t.id AND v.faction_id = ?)
|
||||
|)""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.getCompleteTextsAccessibleTo", sql) { event =>
|
||||
try {
|
||||
stmt.setInt(1, factionId)
|
||||
val rs = stmt.executeQuery()
|
||||
val result = scala.collection.mutable.ArrayBuffer[CompleteClientText]()
|
||||
while rs.next() do
|
||||
result += CompleteClientText(
|
||||
id = rs.getString("id"),
|
||||
text = rs.getString("text"),
|
||||
requestedAfterHistoryCount = rs.getInt("requested_after_history_count")
|
||||
)
|
||||
event.rows = result.size
|
||||
result.toVector
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
def hasAnyTexts: Boolean = {
|
||||
val sql = "SELECT EXISTS(SELECT 1 FROM client_texts)"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.hasAnyTexts", sql) { event =>
|
||||
try {
|
||||
val rs = stmt.executeQuery()
|
||||
val result = rs.next() && rs.getBoolean(1)
|
||||
event.rows = if result then 1 else 0
|
||||
result
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
def importFrom(source: ClientTextStore): Unit = {
|
||||
val originalAutoCommit = connection.getAutoCommit
|
||||
connection.setAutoCommit(false)
|
||||
try {
|
||||
importCompleteTexts(source.completeTexts.values.toVector)
|
||||
importIncompleteTexts(source.incompleteTexts.values.toVector)
|
||||
importUnrequestedTexts(source.unrequestedTexts.values.toVector)
|
||||
source.accessibleTo.foreach {
|
||||
case (textId, factionIds) =>
|
||||
insertVisibility(textId, factionIds)
|
||||
}
|
||||
connection.commit()
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
connection.rollback()
|
||||
throw e
|
||||
} finally connection.setAutoCommit(originalAutoCommit)
|
||||
}
|
||||
|
||||
private def importCompleteTexts(texts: Vector[CompleteClientText]): Unit = {
|
||||
if texts.isEmpty then return
|
||||
|
||||
val sql =
|
||||
"""INSERT INTO client_texts (id, status, text, requested_after_history_count)
|
||||
|VALUES (?, 'complete', ?, ?)
|
||||
|ON CONFLICT DO NOTHING""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.importCompleteTexts", sql) { event =>
|
||||
try {
|
||||
texts.foreach { text =>
|
||||
stmt.setString(1, text.id)
|
||||
stmt.setString(2, text.text)
|
||||
stmt.setInt(3, text.requestedAfterHistoryCount)
|
||||
stmt.addBatch()
|
||||
}
|
||||
event.rows = stmt.executeBatch().sum
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
private def importIncompleteTexts(texts: Vector[IncompleteClientText]): Unit = {
|
||||
if texts.isEmpty then return
|
||||
|
||||
val sql =
|
||||
"""INSERT INTO client_texts
|
||||
| (id, status, text, llm_request, requested_after_history_count, requested_at_millis, last_update_at_millis)
|
||||
|VALUES (?, 'incomplete', ?, ?, ?, ?, ?)
|
||||
|ON CONFLICT DO NOTHING""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.importIncompleteTexts", sql) { event =>
|
||||
try {
|
||||
texts.foreach { text =>
|
||||
stmt.setString(1, text.id)
|
||||
stmt.setString(2, text.partialText)
|
||||
stmt.setBytes(3, GeneratedTextRequestConverter.toProto(text.llmRequest).toByteArray)
|
||||
stmt.setInt(4, text.requestedAfterHistoryCount)
|
||||
stmt.setLong(5, text.requestedAtMillis)
|
||||
stmt.setLong(6, text.lastUpdateAtMillis)
|
||||
stmt.addBatch()
|
||||
}
|
||||
event.rows = stmt.executeBatch().sum
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
private def importUnrequestedTexts(texts: Vector[UnrequestedClientText]): Unit = {
|
||||
if texts.isEmpty then return
|
||||
|
||||
val sql =
|
||||
"""INSERT INTO client_texts (id, status, text, llm_request, requested_after_history_count)
|
||||
|VALUES (?, 'unrequested', '', ?, ?)
|
||||
|ON CONFLICT DO NOTHING""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.importUnrequestedTexts", sql) { event =>
|
||||
try {
|
||||
texts.foreach { text =>
|
||||
stmt.setString(1, text.id)
|
||||
stmt.setBytes(2, GeneratedTextRequestConverter.toProto(text.llmRequest).toByteArray)
|
||||
stmt.setInt(3, text.requestedAfterHistoryCount)
|
||||
stmt.addBatch()
|
||||
}
|
||||
event.rows = stmt.executeBatch().sum
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
private def llmRequestFromResultSet(rs: ResultSet): GeneratedTextRequestT =
|
||||
GeneratedTextRequestConverter.fromProto(
|
||||
net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest.parseFrom(rs.getBytes("llm_request"))
|
||||
)
|
||||
|
||||
private def textExists(id: ClientTextId): Boolean =
|
||||
statusFor(id).nonEmpty
|
||||
|
||||
private def statusFor(id: ClientTextId): Option[String] = {
|
||||
val sql = "SELECT status FROM client_texts WHERE id = ?"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.statusFor", sql) { event =>
|
||||
try {
|
||||
stmt.setString(1, id)
|
||||
val rs = stmt.executeQuery()
|
||||
if rs.next() then {
|
||||
event.rows = 1
|
||||
Some(rs.getString("status"))
|
||||
} else None
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
private def selectIncompleteText(
|
||||
id: ClientTextId
|
||||
): (String, GeneratedTextRequestT, Int, Long) = {
|
||||
val sql =
|
||||
"SELECT status, text, llm_request, requested_after_history_count, requested_at_millis FROM client_texts WHERE id = ?"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.selectIncompleteText", sql) { event =>
|
||||
try {
|
||||
stmt.setString(1, id)
|
||||
val rs = stmt.executeQuery()
|
||||
if !rs.next() then throw new EagleInternalException(s"Text with id $id not found")
|
||||
event.rows = 1
|
||||
val status = rs.getString("status")
|
||||
internalRequire(
|
||||
status == "incomplete",
|
||||
s"Cannot append to text with status $status (expected incomplete)"
|
||||
)
|
||||
(
|
||||
rs.getString("text"),
|
||||
llmRequestFromResultSet(rs),
|
||||
rs.getInt("requested_after_history_count"),
|
||||
rs.getLong("requested_at_millis")
|
||||
)
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
private def readExistingIds(): Set[ClientTextId] = {
|
||||
val sql = "SELECT id FROM client_texts"
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.readExistingIds", sql) { event =>
|
||||
try {
|
||||
val rs = stmt.executeQuery()
|
||||
val result = scala.collection.mutable.Set[ClientTextId]()
|
||||
while rs.next() do result += rs.getString("id")
|
||||
event.rows = result.size
|
||||
result.toSet
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
private def insertVisibility(id: ClientTextId, factionIds: Vector[FactionId]): Unit =
|
||||
insertVisibilityBatch(Vector(id), factionIds)
|
||||
|
||||
private def insertVisibilityBatch(ids: Vector[ClientTextId], factionIds: Vector[FactionId]): Unit = {
|
||||
if ids.isEmpty || factionIds.isEmpty then return
|
||||
|
||||
val sql =
|
||||
"""INSERT INTO client_text_visibility (text_id, faction_id)
|
||||
|VALUES (?, ?)
|
||||
|ON CONFLICT DO NOTHING""".stripMargin
|
||||
val stmt = connection.prepareStatement(sql)
|
||||
JfrEvents.postgresQuery(databaseName, "PostgresClientTextStore.insertVisibilityBatch", sql) { event =>
|
||||
try {
|
||||
for
|
||||
id <- ids
|
||||
factionId <- factionIds
|
||||
do {
|
||||
stmt.setString(1, id)
|
||||
stmt.setInt(2, factionId)
|
||||
stmt.addBatch()
|
||||
}
|
||||
event.rows = stmt.executeBatch().sum
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PostgresClientTextStore {
|
||||
private val SchemaVersion = 1
|
||||
|
||||
def createWithData(
|
||||
gameId: GameId,
|
||||
pregenerated: PregeneratedClientTextStore,
|
||||
unrequestedTexts: Map[ClientTextId, UnrequestedClientText],
|
||||
accessibleTo: Map[ClientTextId, Vector[FactionId]]
|
||||
): PostgresClientTextStore = {
|
||||
val store = create(gameId, pregenerated)
|
||||
val originalAutoCommit = store.connection.getAutoCommit
|
||||
store.connection.setAutoCommit(false)
|
||||
try {
|
||||
val insertSql =
|
||||
"""INSERT INTO client_texts (id, status, text, llm_request, requested_after_history_count)
|
||||
|VALUES (?, 'unrequested', '', ?, ?)""".stripMargin
|
||||
val insertStmt = store.connection.prepareStatement(insertSql)
|
||||
JfrEvents.postgresQuery(store.databaseName, "PostgresClientTextStore.createWithData.insertTexts", insertSql) {
|
||||
event =>
|
||||
try {
|
||||
unrequestedTexts.values.foreach { ut =>
|
||||
insertStmt.setString(1, ut.id)
|
||||
insertStmt.setBytes(2, GeneratedTextRequestConverter.toProto(ut.llmRequest).toByteArray)
|
||||
insertStmt.setInt(3, ut.requestedAfterHistoryCount)
|
||||
insertStmt.addBatch()
|
||||
}
|
||||
event.rows = insertStmt.executeBatch().sum
|
||||
} finally insertStmt.close()
|
||||
}
|
||||
|
||||
accessibleTo.foreach {
|
||||
case (textId, factionIds) =>
|
||||
store.insertVisibility(textId, factionIds)
|
||||
}
|
||||
store.connection.commit()
|
||||
store
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
store.connection.rollback()
|
||||
throw e
|
||||
} finally store.connection.setAutoCommit(originalAutoCommit)
|
||||
end try
|
||||
}
|
||||
|
||||
def loaded(
|
||||
gameId: GameId,
|
||||
pregenerated: PregeneratedClientTextStore
|
||||
): Try[PostgresClientTextStore] = Try {
|
||||
val connection = openConnection()
|
||||
try {
|
||||
val schemaName = schemaNameForGame(gameId)
|
||||
createSchema(connection, schemaName)
|
||||
useSchema(connection, schemaName)
|
||||
initializeSchema(connection)
|
||||
new PostgresClientTextStore(connection, gameId, pregenerated)
|
||||
} catch {
|
||||
case t: Throwable =>
|
||||
connection.close()
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
private def create(
|
||||
gameId: GameId,
|
||||
pregenerated: PregeneratedClientTextStore
|
||||
): PostgresClientTextStore =
|
||||
loaded(gameId, pregenerated).get
|
||||
|
||||
private def openConnection(): Connection = {
|
||||
val cfg = config.getOrElse(
|
||||
throw new EagleInternalException(
|
||||
"PostgresClientTextStore requires EAGLE_POSTGRES_HOST, DATABASE, USER, and PASSWORD"
|
||||
)
|
||||
)
|
||||
Class.forName("org.postgresql.Driver")
|
||||
DriverManager.getConnection(cfg.jdbcUrl, cfg.user, cfg.password)
|
||||
}
|
||||
|
||||
private def config: Option[PostgresConfig] =
|
||||
for
|
||||
host <- envNonEmpty("EAGLE_POSTGRES_HOST")
|
||||
database <- envNonEmpty("EAGLE_POSTGRES_DATABASE")
|
||||
user <- envNonEmpty("EAGLE_POSTGRES_USER")
|
||||
password <- envNonEmpty("EAGLE_POSTGRES_PASSWORD")
|
||||
yield PostgresConfig(
|
||||
host = host,
|
||||
port = sys.env.get("EAGLE_POSTGRES_PORT").flatMap(_.toIntOption).getOrElse(5432),
|
||||
database = database,
|
||||
user = user,
|
||||
password = password,
|
||||
sslmode = sys.env.get("EAGLE_POSTGRES_SSLMODE").filter(_.nonEmpty).getOrElse("require")
|
||||
)
|
||||
|
||||
private def envNonEmpty(name: String): Option[String] =
|
||||
sys.env.get(name).map(_.trim).filter(_.nonEmpty)
|
||||
|
||||
private def schemaNameForGame(gameId: GameId): String = {
|
||||
val schema = "eagle_game_" + java.lang.Long.toHexString(gameId)
|
||||
if !schema.matches("[a-z][a-z0-9_]*") then
|
||||
throw new EagleInternalException(s"Invalid Postgres schema name derived from game id $gameId: $schema")
|
||||
schema
|
||||
}
|
||||
|
||||
private def createSchema(connection: Connection, schemaName: String): Unit = {
|
||||
val stmt = connection.createStatement()
|
||||
try stmt.execute(s"CREATE SCHEMA IF NOT EXISTS $schemaName"): Unit
|
||||
finally stmt.close()
|
||||
}
|
||||
|
||||
private def useSchema(connection: Connection, schemaName: String): Unit = {
|
||||
val stmt = connection.createStatement()
|
||||
try stmt.execute(s"SET search_path TO $schemaName"): Unit
|
||||
finally stmt.close()
|
||||
}
|
||||
|
||||
private def initializeSchema(connection: Connection): Unit = {
|
||||
execEach(
|
||||
connection,
|
||||
Vector("CREATE TABLE IF NOT EXISTS client_text_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
|
||||
)
|
||||
val version = readSchemaVersion(connection)
|
||||
if version == SchemaVersion then ()
|
||||
else if version == 0 then {
|
||||
val originalAutoCommit = connection.getAutoCommit
|
||||
connection.setAutoCommit(false)
|
||||
try {
|
||||
execEach(connection, schemaStatements)
|
||||
writeSchemaVersion(connection, SchemaVersion)
|
||||
connection.commit()
|
||||
} catch {
|
||||
case t: Throwable =>
|
||||
connection.rollback()
|
||||
throw t
|
||||
} finally connection.setAutoCommit(originalAutoCommit)
|
||||
} else
|
||||
throw new EagleInternalException(
|
||||
s"Unsupported Postgres client text schema version $version; expected $SchemaVersion"
|
||||
)
|
||||
end if
|
||||
}
|
||||
|
||||
private def readSchemaVersion(connection: Connection): Int = {
|
||||
val stmt = connection.prepareStatement("SELECT value FROM client_text_metadata WHERE key = 'schema_version'")
|
||||
try {
|
||||
val rs = stmt.executeQuery()
|
||||
if rs.next() then rs.getString(1).toInt else 0
|
||||
} finally stmt.close()
|
||||
}
|
||||
|
||||
private def writeSchemaVersion(connection: Connection, version: Int): Unit = {
|
||||
val stmt = connection.prepareStatement(
|
||||
"""INSERT INTO client_text_metadata (key, value) VALUES ('schema_version', ?)
|
||||
|ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value""".stripMargin
|
||||
)
|
||||
try {
|
||||
stmt.setString(1, version.toString)
|
||||
stmt.executeUpdate(): Unit
|
||||
} finally stmt.close()
|
||||
}
|
||||
|
||||
private def execEach(connection: Connection, statements: Vector[String]): Unit = {
|
||||
val stmt = connection.createStatement()
|
||||
try statements.foreach(sql => stmt.execute(sql))
|
||||
finally stmt.close()
|
||||
}
|
||||
|
||||
private def schemaStatements: Vector[String] =
|
||||
Vector(
|
||||
"""CREATE TABLE client_texts (
|
||||
| id TEXT PRIMARY KEY,
|
||||
| status TEXT NOT NULL CHECK (status IN ('complete', 'incomplete', 'unrequested')),
|
||||
| text TEXT NOT NULL DEFAULT '',
|
||||
| llm_request BYTEA,
|
||||
| requested_after_history_count INTEGER NOT NULL,
|
||||
| requested_at_millis BIGINT,
|
||||
| last_update_at_millis BIGINT
|
||||
|)""".stripMargin,
|
||||
"""CREATE TABLE client_text_visibility (
|
||||
| text_id TEXT NOT NULL,
|
||||
| faction_id INTEGER NOT NULL,
|
||||
| PRIMARY KEY (text_id, faction_id)
|
||||
|)""".stripMargin,
|
||||
"CREATE INDEX idx_client_texts_status ON client_texts(status)"
|
||||
)
|
||||
|
||||
private case class PostgresConfig(
|
||||
host: String,
|
||||
port: Int,
|
||||
database: String,
|
||||
user: String,
|
||||
password: String,
|
||||
sslmode: String
|
||||
) {
|
||||
def jdbcUrl: String =
|
||||
s"jdbc:postgresql://$host:$port/$database?sslmode=$sslmode"
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package net.eagle0.eagle.service
|
||||
import java.sql.{Connection, ResultSet}
|
||||
|
||||
import net.eagle0.common.JfrEvents
|
||||
import net.eagle0.eagle.{BattalionId, FactionId, HeroId}
|
||||
import net.eagle0.eagle.common.command_type.command_type.CommandType as CommandTypeProto
|
||||
import net.eagle0.eagle.common.eagle_map_info.EagleMapInfo as EagleMapInfoProto
|
||||
import net.eagle0.eagle.common.game_type.GameType as GameTypeProto
|
||||
@@ -12,17 +11,7 @@ import net.eagle0.eagle.common.tutorial_phase.TutorialPhase as TutorialPhaseProt
|
||||
import net.eagle0.eagle.internal.chronicle_entry.ChronicleEntry as ChronicleEntryProto
|
||||
import net.eagle0.eagle.internal.shardok_battle.ShardokBattle as ShardokBattleProto
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.{
|
||||
ActionResultT,
|
||||
ChangedBattalionT,
|
||||
ChangedFactionT,
|
||||
ChangedHeroT,
|
||||
ClientTextVisibilityExtensionT,
|
||||
NotificationT
|
||||
}
|
||||
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
|
||||
import net.eagle0.eagle.model.action_result.concrete.ActionResultC
|
||||
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
|
||||
import net.eagle0.eagle.model.action_result.types.ActionResultType
|
||||
import net.eagle0.eagle.model.proto_converters.{
|
||||
CommandTypeConverter,
|
||||
@@ -32,12 +21,7 @@ import net.eagle0.eagle.model.proto_converters.{
|
||||
}
|
||||
import net.eagle0.eagle.model.proto_converters.chronicle_entry.ChronicleEntryConverter
|
||||
import net.eagle0.eagle.model.proto_converters.shardok_battle.ShardokBattleConverter
|
||||
import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
import net.eagle0.eagle.model.state.date.Date
|
||||
import net.eagle0.eagle.model.state.faction.FactionT
|
||||
import net.eagle0.eagle.model.state.hero.HeroT
|
||||
import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
import net.eagle0.eagle.model.state.BattalionType
|
||||
|
||||
/**
|
||||
* Top-level orchestrator that assembles a domain `ActionResultC` for a given `actionSeq` from the SQLite tables built
|
||||
@@ -84,28 +68,6 @@ private[service] object ActionResultDbReader {
|
||||
}
|
||||
}
|
||||
|
||||
def readRangePostgres(connection: Connection, startInclusive: Int, endExclusive: Int): Vector[(Int, ActionResultT)] =
|
||||
JfrEvents.withPostgresQueryEvents {
|
||||
JfrEvents.postgresQuery("postgres-history", "ActionResultDbReader.readRange", TopLevelRangeSql) { event =>
|
||||
val children = RangeChildRows.read(connection, startInclusive, endExclusive)
|
||||
val ps = connection.prepareStatement(TopLevelRangeSql)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[(Int, ActionResultT)]
|
||||
var rows = 0
|
||||
while rs.next() do {
|
||||
rows += 1
|
||||
val actionSeq = rs.getInt("action_seq")
|
||||
builder += ((actionSeq, fromTopLevelRow(actionSeq, rs, children)))
|
||||
}
|
||||
event.rows = rows
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
private val TopLevelSql: String =
|
||||
"""SELECT action_result_type,
|
||||
| acting_hero_id, acting_faction_id, province_id, province_id_acted,
|
||||
@@ -116,17 +78,6 @@ private[service] object ActionResultDbReader {
|
||||
| FROM action_results
|
||||
| WHERE action_seq = ?""".stripMargin
|
||||
|
||||
private val TopLevelRangeSql: String =
|
||||
"""SELECT action_seq, action_result_type,
|
||||
| acting_hero_id, acting_faction_id, province_id, province_id_acted,
|
||||
| new_round_phase, last_command_type, new_game_type, new_tutorial_phase,
|
||||
| new_round_id, new_victor_faction_id, game_ended, new_random_seed,
|
||||
| new_date_year, new_date_month, resolved_battle,
|
||||
| new_battle_proto, new_chronicle_entry_proto, new_eagle_map_proto
|
||||
| FROM action_results
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq""".stripMargin
|
||||
|
||||
private def readTopLevel(connection: Connection, actionSeq: Int, recordRows: Int => Unit): ActionResultC = {
|
||||
val ps = connection.prepareStatement(TopLevelSql)
|
||||
try {
|
||||
@@ -142,9 +93,6 @@ private[service] object ActionResultDbReader {
|
||||
}
|
||||
|
||||
private def fromTopLevelRow(connection: Connection, actionSeq: Int, rs: ResultSet): ActionResultC =
|
||||
fromTopLevelRow(actionSeq, rs, RangeChildRows.readSingle(connection, actionSeq))
|
||||
|
||||
private def fromTopLevelRow(actionSeq: Int, rs: ResultSet, children: RangeChildRows): ActionResultC =
|
||||
ActionResultC(
|
||||
actionResultType = ActionResultType.fromValue(rs.getInt("action_result_type")),
|
||||
actingHeroId = getOptInt(rs, "acting_hero_id"),
|
||||
@@ -167,95 +115,31 @@ private[service] object ActionResultDbReader {
|
||||
resolvedBattle = Option(rs.getString("resolved_battle")),
|
||||
newChronicleEntry = Option(rs.getBytes("new_chronicle_entry_proto"))
|
||||
.map(b => ChronicleEntryConverter.fromProto(ChronicleEntryProto.parseFrom(b))),
|
||||
changedBattalions = children.changedBattalions.getOrElse(actionSeq, Vector.empty),
|
||||
changedFactions = children.changedFactions.getOrElse(actionSeq, Vector.empty),
|
||||
changedHeroes = children.changedHeroes.getOrElse(actionSeq, Vector.empty),
|
||||
changedProvinces = children.changedProvinces.getOrElse(actionSeq, Vector.empty),
|
||||
newBattalions = children.newBattalions.getOrElse(actionSeq, Vector.empty),
|
||||
newFactions = children.newFactions.getOrElse(actionSeq, Vector.empty),
|
||||
newHeroes = children.newHeroes.getOrElse(actionSeq, Vector.empty),
|
||||
newProvinces = children.newProvinces.getOrElse(actionSeq, Vector.empty),
|
||||
newBattalionTypes = children.newBattalionTypes.getOrElse(actionSeq, Vector.empty),
|
||||
destroyedBattalionIds = children.destroyedBattalionIds.getOrElse(actionSeq, Vector.empty),
|
||||
removedHeroIds = children.removedHeroIds.getOrElse(actionSeq, Vector.empty),
|
||||
removedFactionIds = children.removedFactionIds.getOrElse(actionSeq, Vector.empty),
|
||||
clientTextVisibilityExtensions = children.clientTextVisibilityExtensions.getOrElse(actionSeq, Vector.empty),
|
||||
newGeneratedTextRequests = children.newGeneratedTextRequests.getOrElse(actionSeq, Vector.empty),
|
||||
newNotifications = children.newNotifications.getOrElse(actionSeq, Vector.empty),
|
||||
removedNotifications = children.removedNotifications.getOrElse(actionSeq, Vector.empty),
|
||||
changedBattalions = ChangedBattalionTableReader.read(connection, actionSeq),
|
||||
changedFactions = ChangedFactionTableReader.read(connection, actionSeq),
|
||||
changedHeroes = ChangedHeroTableReader.read(connection, actionSeq),
|
||||
changedProvinces = ChangedProvinceTableReader.read(connection, actionSeq),
|
||||
newBattalions = NewBattalionTableReader.read(connection, actionSeq),
|
||||
newFactions = NewFactionTableReader.read(connection, actionSeq),
|
||||
newHeroes = NewHeroTableReader.read(connection, actionSeq),
|
||||
newProvinces = NewProvinceTableReader.read(connection, actionSeq),
|
||||
newBattalionTypes = ActionResultVectorTablesReader.readNewBattalionTypes(connection, actionSeq),
|
||||
destroyedBattalionIds = ActionResultVectorTablesReader.readDestroyedBattalionIds(connection, actionSeq),
|
||||
removedHeroIds = ActionResultVectorTablesReader.readRemovedHeroIds(connection, actionSeq),
|
||||
removedFactionIds = ActionResultVectorTablesReader.readRemovedFactionIds(connection, actionSeq),
|
||||
clientTextVisibilityExtensions = ClientTextVisibilityExtensionTableReader.read(connection, actionSeq),
|
||||
newGeneratedTextRequests = GeneratedTextRequestTableReader.read(connection, actionSeq),
|
||||
newNotifications = NotificationTableReader.readNew(connection, actionSeq),
|
||||
removedNotifications = NotificationTableReader.readRemoved(connection, actionSeq),
|
||||
newVictorFactionId = getOptInt(rs, "new_victor_faction_id"),
|
||||
gameEnded = getOptInt(rs, "game_ended").map(_ != 0),
|
||||
newRandomSeed = getOptLong(rs, "new_random_seed"),
|
||||
newGameType = decodeGameType(rs),
|
||||
newEagleMap = Option(rs.getBytes("new_eagle_map_proto"))
|
||||
.map(b => EagleMapInfoConverter.fromProto(EagleMapInfoProto.parseFrom(b))),
|
||||
affectedFactionIds = children.affectedFactionIds.getOrElse(actionSeq, Vector.empty)
|
||||
affectedFactionIds = ActionResultVectorTablesReader.readAffectedFactionIds(connection, actionSeq)
|
||||
)
|
||||
|
||||
private case class RangeChildRows(
|
||||
changedBattalions: Map[Int, Vector[ChangedBattalionT]],
|
||||
changedFactions: Map[Int, Vector[ChangedFactionT]],
|
||||
changedHeroes: Map[Int, Vector[ChangedHeroT]],
|
||||
changedProvinces: Map[Int, Vector[ChangedProvinceT]],
|
||||
newBattalions: Map[Int, Vector[BattalionT]],
|
||||
newFactions: Map[Int, Vector[FactionT]],
|
||||
newHeroes: Map[Int, Vector[HeroT]],
|
||||
newProvinces: Map[Int, Vector[ProvinceT]],
|
||||
newBattalionTypes: Map[Int, Vector[BattalionType]],
|
||||
destroyedBattalionIds: Map[Int, Vector[BattalionId]],
|
||||
removedHeroIds: Map[Int, Vector[HeroId]],
|
||||
removedFactionIds: Map[Int, Vector[FactionId]],
|
||||
clientTextVisibilityExtensions: Map[Int, Vector[ClientTextVisibilityExtensionT]],
|
||||
newGeneratedTextRequests: Map[Int, Vector[GeneratedTextRequestT]],
|
||||
newNotifications: Map[Int, Vector[NotificationT]],
|
||||
removedNotifications: Map[Int, Vector[NotificationT]],
|
||||
affectedFactionIds: Map[Int, Vector[FactionId]]
|
||||
)
|
||||
|
||||
private object RangeChildRows {
|
||||
def readSingle(connection: Connection, actionSeq: Int): RangeChildRows =
|
||||
read(connection, actionSeq, actionSeq + 1)
|
||||
|
||||
def read(connection: Connection, startInclusive: Int, endExclusive: Int): RangeChildRows =
|
||||
RangeChildRows(
|
||||
changedBattalions = ChangedBattalionTableReader.readRange(connection, startInclusive, endExclusive),
|
||||
changedFactions = ChangedFactionTableReader.readRange(connection, startInclusive, endExclusive),
|
||||
changedHeroes = ChangedHeroTableReader.readRange(connection, startInclusive, endExclusive),
|
||||
changedProvinces = ChangedProvinceTableReader.readRange(connection, startInclusive, endExclusive),
|
||||
newBattalions = NewBattalionTableReader.readRange(connection, startInclusive, endExclusive),
|
||||
newFactions = NewFactionTableReader.readRange(connection, startInclusive, endExclusive),
|
||||
newHeroes = NewHeroTableReader.readRange(connection, startInclusive, endExclusive),
|
||||
newProvinces = NewProvinceTableReader.readRange(connection, startInclusive, endExclusive),
|
||||
newBattalionTypes = ActionResultVectorTablesReader.readNewBattalionTypesRange(
|
||||
connection,
|
||||
startInclusive,
|
||||
endExclusive
|
||||
),
|
||||
destroyedBattalionIds = ActionResultVectorTablesReader.readDestroyedBattalionIdsRange(
|
||||
connection,
|
||||
startInclusive,
|
||||
endExclusive
|
||||
),
|
||||
removedHeroIds =
|
||||
ActionResultVectorTablesReader.readRemovedHeroIdsRange(connection, startInclusive, endExclusive),
|
||||
removedFactionIds = ActionResultVectorTablesReader.readRemovedFactionIdsRange(
|
||||
connection,
|
||||
startInclusive,
|
||||
endExclusive
|
||||
),
|
||||
clientTextVisibilityExtensions = ClientTextVisibilityExtensionTableReader.readRange(
|
||||
connection,
|
||||
startInclusive,
|
||||
endExclusive
|
||||
),
|
||||
newGeneratedTextRequests = GeneratedTextRequestTableReader.readRange(connection, startInclusive, endExclusive),
|
||||
newNotifications = NotificationTableReader.readNewRange(connection, startInclusive, endExclusive),
|
||||
removedNotifications = NotificationTableReader.readRemovedRange(connection, startInclusive, endExclusive),
|
||||
affectedFactionIds =
|
||||
ActionResultVectorTablesReader.readAffectedFactionIdsRange(connection, startInclusive, endExclusive)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode `(new_game_type, new_tutorial_phase)` into `Option[GameType]`. NULL `new_game_type` → None. Non-NULL
|
||||
* `new_game_type` is the GameType proto enum value; `new_tutorial_phase`, when set, is the TutorialPhase proto enum
|
||||
|
||||
@@ -23,46 +23,18 @@ private[service] object ActionResultVectorTablesReader {
|
||||
def readDestroyedBattalionIds(connection: Connection, actionSeq: Int): Vector[BattalionId] =
|
||||
readIds(connection, "action_destroyed_battalion_ids", "battalion_id", actionSeq)
|
||||
|
||||
def readDestroyedBattalionIdsRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[BattalionId]] =
|
||||
readIdsRange(connection, "action_destroyed_battalion_ids", "battalion_id", startInclusive, endExclusive)
|
||||
|
||||
/** Read `removedHeroIds` from `action_removed_hero_ids`, ordered by `n`. */
|
||||
def readRemovedHeroIds(connection: Connection, actionSeq: Int): Vector[HeroId] =
|
||||
readIds(connection, "action_removed_hero_ids", "hero_id", actionSeq)
|
||||
|
||||
def readRemovedHeroIdsRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[HeroId]] =
|
||||
readIdsRange(connection, "action_removed_hero_ids", "hero_id", startInclusive, endExclusive)
|
||||
|
||||
/** Read `removedFactionIds` from `action_removed_faction_ids`, ordered by `n`. */
|
||||
def readRemovedFactionIds(connection: Connection, actionSeq: Int): Vector[FactionId] =
|
||||
readIds(connection, "action_removed_faction_ids", "faction_id", actionSeq)
|
||||
|
||||
def readRemovedFactionIdsRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[FactionId]] =
|
||||
readIdsRange(connection, "action_removed_faction_ids", "faction_id", startInclusive, endExclusive)
|
||||
|
||||
/** Read `affectedFactionIds` from `action_affected_faction_ids`, ordered by `n`. */
|
||||
def readAffectedFactionIds(connection: Connection, actionSeq: Int): Vector[FactionId] =
|
||||
readIds(connection, "action_affected_faction_ids", "faction_id", actionSeq)
|
||||
|
||||
def readAffectedFactionIdsRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[FactionId]] =
|
||||
readIdsRange(connection, "action_affected_faction_ids", "faction_id", startInclusive, endExclusive)
|
||||
|
||||
/**
|
||||
* Read `newBattalionTypes` from `action_new_battalion_types`, ordered by `n`. The `type_id` column is ignored on read
|
||||
* — the blob is authoritative; the column exists only as a queryable handle for analytics.
|
||||
@@ -81,31 +53,6 @@ private[service] object ActionResultVectorTablesReader {
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
def readNewBattalionTypesRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[BattalionType]] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, battalion_type_proto
|
||||
| FROM action_new_battalion_types
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders =
|
||||
scala.collection.mutable.Map.empty[Int, scala.collection.mutable.Builder[BattalionType, Vector[BattalionType]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += BattalionTypeConverter.fromProto(
|
||||
BattalionTypeProto.parseFrom(rs.getBytes("battalion_type_proto"))
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all rows from a primitive-ID join table for `actionSeq`, ordered by `n`. `table` and `idColumn` are internal
|
||||
* constants, never user input.
|
||||
@@ -122,27 +69,4 @@ private[service] object ActionResultVectorTablesReader {
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readIdsRange(
|
||||
connection: Connection,
|
||||
table: String,
|
||||
idColumn: String,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[Int]] = {
|
||||
val ps = connection.prepareStatement(
|
||||
s"""SELECT action_seq, $idColumn
|
||||
| FROM $table
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders = scala.collection.mutable.Map.empty[Int, scala.collection.mutable.Builder[Int, Vector[Int]]]
|
||||
while rs.next() do builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += rs.getInt(idColumn)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +277,6 @@ scala_library(
|
||||
"//src/main/scala/net/eagle0/eagle/client_text",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store_impl",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:postgres_client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:sqlite_client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
|
||||
|
||||
@@ -21,27 +21,19 @@ import net.eagle0.eagle.model.proto_converters.BattalionConverter
|
||||
private[service] object ChangedBattalionTableReader {
|
||||
|
||||
/** Read every `ChangedBattalion` for `actionSeq`, ordered by `n` (original vector order). */
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ChangedBattalionT] =
|
||||
readRange(connection, actionSeq, actionSeq + 1).getOrElse(actionSeq, Vector.empty)
|
||||
|
||||
def readRange(connection: Connection, startInclusive: Int, endExclusive: Int): Map[Int, Vector[ChangedBattalionT]] = {
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ChangedBattalionT] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, changed_battalion_proto
|
||||
| FROM action_changed_battalions
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
"SELECT changed_battalion_proto FROM action_changed_battalions WHERE action_seq = ? ORDER BY n"
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders = scala.collection.mutable.Map
|
||||
.empty[Int, scala.collection.mutable.Builder[ChangedBattalionT, Vector[ChangedBattalionT]]]
|
||||
ps.setInt(1, actionSeq)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[ChangedBattalionT]
|
||||
while rs.next() do {
|
||||
val battalion = BattalionConverter.fromProto(BattalionProto.parseFrom(rs.getBytes("changed_battalion_proto")))
|
||||
builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += ChangedBattalionC(battalion)
|
||||
builder += ChangedBattalionC(battalion)
|
||||
}
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package net.eagle0.eagle.service
|
||||
|
||||
import java.sql.{Connection, ResultSet}
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
import net.eagle0.eagle.common.diplomacy_offer.DiplomacyOffer as DiplomacyOfferProto
|
||||
import net.eagle0.eagle.model.action_result.concrete.ChangedFactionC
|
||||
import net.eagle0.eagle.model.action_result.concrete.ChangedFactionC.{
|
||||
@@ -119,133 +117,6 @@ private[service] object ChangedFactionTableReader {
|
||||
}
|
||||
}
|
||||
|
||||
def readRange(connection: Connection, startInclusive: Int, endExclusive: Int): Map[Int, Vector[ChangedFactionT]] = {
|
||||
val parents: Vector[(Int, Int, ChangedFactionC)] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, n, faction_id, new_faction_head_hero_id, new_focus_province_id, clear_focus_province_id,
|
||||
| new_last_acted_province_id, clear_last_acted_province_id, new_name
|
||||
| FROM action_changed_factions
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[(Int, Int, ChangedFactionC)]
|
||||
while rs.next() do builder += ((rs.getInt("action_seq"), rs.getInt("n"), readParentRow(rs)))
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
if parents.isEmpty then Map.empty
|
||||
else {
|
||||
val newLeaderHeroIds =
|
||||
readIdsRange(connection, "action_changed_factions_new_leader_hero_ids", "hero_id", startInclusive, endExclusive)
|
||||
val removedLeaderHeroIds = readIdsRange(
|
||||
connection,
|
||||
"action_changed_factions_removed_leader_hero_ids",
|
||||
"hero_id",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val newPrestigeModifiers = readPrestigeModifiersRange(
|
||||
connection,
|
||||
"action_changed_factions_new_prestige_modifiers",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val removedPrestigeModifiers = readPrestigeModifiersRange(
|
||||
connection,
|
||||
"action_changed_factions_removed_prestige_modifiers",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val changedFactionRelationships = readFactionRelationshipsRange(connection, startInclusive, endExclusive)
|
||||
val removedRelationshipTargetIds = readIdsRange(
|
||||
connection,
|
||||
"action_changed_factions_removed_relationship_target_ids",
|
||||
"target_faction_id",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val newIncomingDiplomacyOffers = readDiplomacyOffersRange(connection, startInclusive, endExclusive)
|
||||
val removedIncomingOfferOriginIds = readIdsRange(
|
||||
connection,
|
||||
"action_changed_factions_removed_incoming_diplomacy_offer_origin_ids",
|
||||
"origin_faction_id",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val newOutgoingTruceOfferTargetIds = readIdsRange(
|
||||
connection,
|
||||
"action_changed_factions_new_outgoing_truce_offer_target_ids",
|
||||
"target_faction_id",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val newOutgoingAllianceOfferTargetIds = readIdsRange(
|
||||
connection,
|
||||
"action_changed_factions_new_outgoing_alliance_offer_target_ids",
|
||||
"target_faction_id",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val newOutgoingInvitationTargetIds = readIdsRange(
|
||||
connection,
|
||||
"action_changed_factions_new_outgoing_invitation_target_ids",
|
||||
"target_faction_id",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val newOutgoingRansomOfferTargetIds = readIdsRange(
|
||||
connection,
|
||||
"action_changed_factions_new_outgoing_ransom_offer_target_ids",
|
||||
"target_faction_id",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val updatedReconnedProvinces = readProvinceViewsRange(connection, startInclusive, endExclusive)
|
||||
val removedReconnedProvinceIds = readIdsRange(
|
||||
connection,
|
||||
"action_changed_factions_removed_reconned_province_ids",
|
||||
"province_id",
|
||||
startInclusive,
|
||||
endExclusive
|
||||
)
|
||||
val trustLevelUpdates = readTrustLevelUpdatesRange(connection, startInclusive, endExclusive)
|
||||
|
||||
val builders = mutable.Map.empty[Int, scala.collection.mutable.Builder[ChangedFactionT, Vector[ChangedFactionT]]]
|
||||
parents.foreach {
|
||||
case (actionSeq, n, cf) =>
|
||||
val key = (actionSeq, n)
|
||||
builders.getOrElseUpdate(actionSeq, Vector.newBuilder) += cf.copy(
|
||||
newLeaderHeroIds = newLeaderHeroIds.getOrElse(key, Vector.empty),
|
||||
removedLeaderHeroIds = removedLeaderHeroIds.getOrElse(key, Vector.empty),
|
||||
newPrestigeModifiers = newPrestigeModifiers.getOrElse(key, Vector.empty),
|
||||
removedPrestigeModifiers = removedPrestigeModifiers.getOrElse(key, Vector.empty),
|
||||
changedFactionRelationships = changedFactionRelationships.getOrElse(key, Vector.empty),
|
||||
removedFactionRelationshipFactionIds = removedRelationshipTargetIds.getOrElse(key, Vector.empty),
|
||||
newIncomingDiplomacyOffers = newIncomingDiplomacyOffers.getOrElse(key, Vector.empty),
|
||||
removedIncomingDiplomacyOfferFactionIds = removedIncomingOfferOriginIds.getOrElse(key, Vector.empty),
|
||||
newOutgoingTruceOfferFactionIds =
|
||||
newOutgoingTruceOfferTargetIds.getOrElse(key, Vector.empty).map(OutgoingTruceOfferFactionId.apply),
|
||||
newOutgoingAllianceOfferFactionIds =
|
||||
newOutgoingAllianceOfferTargetIds.getOrElse(key, Vector.empty).map(OutgoingAllianceOfferFactionId.apply),
|
||||
newOutgoingInvitationFactionIds =
|
||||
newOutgoingInvitationTargetIds.getOrElse(key, Vector.empty).map(OutgoingInvitationFactionId.apply),
|
||||
newOutgoingRansomOfferFactionIds =
|
||||
newOutgoingRansomOfferTargetIds.getOrElse(key, Vector.empty).map(OutgoingRansomOfferFactionId.apply),
|
||||
updatedReconnedProvinces = updatedReconnedProvinces.getOrElse(key, Vector.empty),
|
||||
removedReconnedProvinceIds = removedReconnedProvinceIds.getOrElse(key, Vector.empty),
|
||||
trustLevelUpdates = trustLevelUpdates.getOrElse(key, Vector.empty)
|
||||
)
|
||||
}
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
}
|
||||
end if
|
||||
}
|
||||
|
||||
/** Tier-1 scalar columns into a `ChangedFactionC` with all Vector fields left at their defaults. */
|
||||
private def readParentRow(rs: ResultSet): ChangedFactionC =
|
||||
ChangedFactionC(
|
||||
@@ -273,30 +144,6 @@ private[service] object ChangedFactionTableReader {
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readIdsRange(
|
||||
connection: Connection,
|
||||
table: String,
|
||||
idColumn: String,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[(Int, Int), Vector[Int]] = {
|
||||
val ps = connection.prepareStatement(
|
||||
s"""SELECT action_seq, n, $idColumn
|
||||
| FROM $table
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n, $idColumn""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders = mutable.Map.empty[(Int, Int), scala.collection.mutable.Builder[Int, Vector[Int]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate((rs.getInt("action_seq"), rs.getInt("n")), Vector.newBuilder) += rs.getInt(idColumn)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readPrestigeModifiers(
|
||||
connection: Connection,
|
||||
table: String,
|
||||
@@ -315,33 +162,6 @@ private[service] object ChangedFactionTableReader {
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readPrestigeModifiersRange(
|
||||
connection: Connection,
|
||||
table: String,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[(Int, Int), Vector[PrestigeModifier]] = {
|
||||
val ps = connection.prepareStatement(
|
||||
s"""SELECT action_seq, n, kind, value
|
||||
| FROM $table
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n, i""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders =
|
||||
mutable.Map.empty[(Int, Int), scala.collection.mutable.Builder[PrestigeModifier, Vector[PrestigeModifier]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate((rs.getInt("action_seq"), rs.getInt("n")), Vector.newBuilder) += PrestigeModifier(
|
||||
prestigeTypeFromString(rs.getString("kind")),
|
||||
rs.getDouble("value")
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readFactionRelationships(connection: Connection, actionSeq: Int, n: Int): Vector[FactionRelationship] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT target_faction_id, relationship_level, reset_date_year, reset_date_month, trust_value
|
||||
@@ -370,41 +190,6 @@ private[service] object ChangedFactionTableReader {
|
||||
end try
|
||||
}
|
||||
|
||||
private def readFactionRelationshipsRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[(Int, Int), Vector[FactionRelationship]] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, n, target_faction_id, relationship_level, reset_date_year, reset_date_month, trust_value
|
||||
| FROM action_changed_factions_changed_relationships
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n, i""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders = mutable.Map
|
||||
.empty[(Int, Int), scala.collection.mutable.Builder[FactionRelationship, Vector[FactionRelationship]]]
|
||||
while rs.next() do {
|
||||
val resetDate = (getIntOpt(rs, "reset_date_year"), getIntOpt(rs, "reset_date_month")) match {
|
||||
case (Some(y), Some(m)) => Some(Date(year = y, month = Date.Month.fromInt(m)))
|
||||
case _ => None
|
||||
}
|
||||
builders.getOrElseUpdate((rs.getInt("action_seq"), rs.getInt("n")), Vector.newBuilder) +=
|
||||
FactionRelationship(
|
||||
targetFactionId = rs.getInt("target_faction_id"),
|
||||
relationshipLevel = relationshipLevelFromString(rs.getString("relationship_level")),
|
||||
resetDate = resetDate,
|
||||
trustValue = rs.getInt("trust_value")
|
||||
)
|
||||
}
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally ps.close()
|
||||
end try
|
||||
}
|
||||
|
||||
private def readTrustLevelUpdates(connection: Connection, actionSeq: Int, n: Int): Vector[TrustLevelUpdate] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"SELECT target_faction_id, delta FROM action_changed_factions_trust_level_updates " +
|
||||
@@ -421,32 +206,6 @@ private[service] object ChangedFactionTableReader {
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readTrustLevelUpdatesRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[(Int, Int), Vector[TrustLevelUpdate]] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, n, target_faction_id, delta
|
||||
| FROM action_changed_factions_trust_level_updates
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n, i""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders =
|
||||
mutable.Map.empty[(Int, Int), scala.collection.mutable.Builder[TrustLevelUpdate, Vector[TrustLevelUpdate]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate((rs.getInt("action_seq"), rs.getInt("n")), Vector.newBuilder) += TrustLevelUpdate(
|
||||
targetFactionId = rs.getInt("target_faction_id"),
|
||||
delta = rs.getInt("delta")
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readDiplomacyOffers(connection: Connection, actionSeq: Int, n: Int): Vector[DiplomacyOffer] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"SELECT offer_proto FROM action_changed_factions_new_incoming_diplomacy_offers " +
|
||||
@@ -463,30 +222,6 @@ private[service] object ChangedFactionTableReader {
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readDiplomacyOffersRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[(Int, Int), Vector[DiplomacyOffer]] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, n, offer_proto
|
||||
| FROM action_changed_factions_new_incoming_diplomacy_offers
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n, i""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders =
|
||||
mutable.Map.empty[(Int, Int), scala.collection.mutable.Builder[DiplomacyOffer, Vector[DiplomacyOffer]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate((rs.getInt("action_seq"), rs.getInt("n")), Vector.newBuilder) +=
|
||||
DiplomacyOfferConverter.fromProto(DiplomacyOfferProto.parseFrom(rs.getBytes("offer_proto")))
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readProvinceViews(connection: Connection, actionSeq: Int, n: Int): Vector[ProvinceView] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"SELECT province_view_proto FROM action_changed_factions_updated_reconned_provinces " +
|
||||
@@ -503,29 +238,6 @@ private[service] object ChangedFactionTableReader {
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readProvinceViewsRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[(Int, Int), Vector[ProvinceView]] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, n, province_view_proto
|
||||
| FROM action_changed_factions_updated_reconned_provinces
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n, i""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders = mutable.Map.empty[(Int, Int), scala.collection.mutable.Builder[ProvinceView, Vector[ProvinceView]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate((rs.getInt("action_seq"), rs.getInt("n")), Vector.newBuilder) +=
|
||||
ProvinceViewConverter.fromProto(ProvinceViewProto.parseFrom(rs.getBytes("province_view_proto")))
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
/** Inverse of `PrestigeModifier.Type.toString` (the writer's `kind` column). */
|
||||
private def prestigeTypeFromString(kind: String): PrestigeModifier.Type = kind match {
|
||||
case "Unknown" => PrestigeModifier.Type.Unknown
|
||||
|
||||
@@ -2,8 +2,6 @@ package net.eagle0.eagle.service
|
||||
|
||||
import java.sql.{Connection, ResultSet}
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
import net.eagle0.eagle.common.profession.Profession as ProfessionProto
|
||||
import net.eagle0.eagle.internal.event_for_hero_backstory.EventForHeroBackstory as EventForHeroBackstoryProto
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ChangedHeroC, StatAbsolute, StatChange, StatDelta, StatNoChange}
|
||||
@@ -26,77 +24,56 @@ import net.eagle0.eagle.model.state.hero.EventForHeroBackstoryT
|
||||
private[service] object ChangedHeroTableReader {
|
||||
|
||||
/** Read every `ChangedHero` for `actionSeq` from the v4 tables, ordered by `n` (original `changedHeroes` order). */
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ChangedHeroT] =
|
||||
readRange(connection, actionSeq, actionSeq + 1).getOrElse(actionSeq, Vector.empty)
|
||||
|
||||
def readRange(connection: Connection, startInclusive: Int, endExclusive: Int): Map[Int, Vector[ChangedHeroT]] = {
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ChangedHeroT] = {
|
||||
// Pass 1: fetch each hero's row (Tier-1 + Tier-2 columns). ResultSet is drained and closed before the per-hero
|
||||
// event queries below run (xerial sqlite-jdbc dislikes interleaving statements with an open ResultSet).
|
||||
val withoutEvents: Vector[(Int, Int, ChangedHeroC)] = {
|
||||
val withoutEvents: Vector[(Int, ChangedHeroC)] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, n, hero_id, clear_faction_id, new_faction_id, strength_xp_delta, agility_xp_delta,
|
||||
"""SELECT n, hero_id, clear_faction_id, new_faction_id, strength_xp_delta, agility_xp_delta,
|
||||
| charisma_xp_delta, wisdom_xp_delta, constitution_xp_delta, strength_delta, agility_delta,
|
||||
| wisdom_delta, charisma_delta, constitution_delta, new_profession, new_backstory_text_id,
|
||||
| clear_events_for_hero_backstory, loyalty_delta, loyalty_absolute, vigor_delta, vigor_absolute
|
||||
| FROM action_changed_heroes
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
| FROM action_changed_heroes WHERE action_seq = ? ORDER BY n""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
ps.setInt(1, actionSeq)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[(Int, Int, ChangedHeroC)]
|
||||
val builder = Vector.newBuilder[(Int, ChangedHeroC)]
|
||||
while rs.next() do {
|
||||
val actionSeq = rs.getInt("action_seq")
|
||||
val n = rs.getInt("n")
|
||||
builder += ((actionSeq, n, readHeroRow(rs)))
|
||||
val n = rs.getInt("n")
|
||||
builder += ((n, readHeroRow(rs)))
|
||||
}
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
if withoutEvents.isEmpty then Map.empty
|
||||
if withoutEvents.isEmpty then Vector.empty
|
||||
else {
|
||||
val events = readEventsRange(connection, startInclusive, endExclusive)
|
||||
val builders = mutable.Map.empty[Int, scala.collection.mutable.Builder[ChangedHeroT, Vector[ChangedHeroT]]]
|
||||
withoutEvents.foreach {
|
||||
case (actionSeq, n, h) =>
|
||||
builders.getOrElseUpdate(actionSeq, Vector.newBuilder) += h.copy(
|
||||
newEventsForHeroBackstory = events.getOrElse((actionSeq, n), Vector.empty)
|
||||
)
|
||||
}
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
}
|
||||
}
|
||||
|
||||
private def readEventsRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[(Int, Int), Vector[EventForHeroBackstoryT]] = {
|
||||
val eventsStmt = connection.prepareStatement(
|
||||
"""SELECT action_seq, n, event_proto
|
||||
| FROM action_changed_heroes_new_backstory_events
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n, i""".stripMargin
|
||||
)
|
||||
try {
|
||||
eventsStmt.setInt(1, startInclusive)
|
||||
eventsStmt.setInt(2, endExclusive)
|
||||
val rs = eventsStmt.executeQuery()
|
||||
val builders = mutable.Map
|
||||
.empty[(Int, Int), scala.collection.mutable.Builder[EventForHeroBackstoryT, Vector[EventForHeroBackstoryT]]]
|
||||
// Pass 2: per-hero Tier-3 events. Reuse one PreparedStatement across heroes. Filter by (action_seq, n) to
|
||||
// disambiguate when two ChangedHeroes share a hero_id.
|
||||
val eventsStmt = connection.prepareStatement(
|
||||
"""SELECT event_proto FROM action_changed_heroes_new_backstory_events
|
||||
| WHERE action_seq = ? AND n = ? ORDER BY i""".stripMargin
|
||||
)
|
||||
try
|
||||
while rs.next() do {
|
||||
val proto = EventForHeroBackstoryProto.parseFrom(rs.getBytes("event_proto"))
|
||||
builders.getOrElseUpdate((rs.getInt("action_seq"), rs.getInt("n")), Vector.newBuilder) +=
|
||||
EventForHeroBackstoryConverter.fromProto(proto)
|
||||
withoutEvents.map {
|
||||
case (n, h) =>
|
||||
eventsStmt.setInt(1, actionSeq)
|
||||
eventsStmt.setInt(2, n)
|
||||
val rs = eventsStmt.executeQuery()
|
||||
val builder = Vector.newBuilder[EventForHeroBackstoryT]
|
||||
try
|
||||
while rs.next() do {
|
||||
val proto = EventForHeroBackstoryProto.parseFrom(rs.getBytes("event_proto"))
|
||||
builder += EventForHeroBackstoryConverter.fromProto(proto)
|
||||
}
|
||||
finally rs.close()
|
||||
h.copy(newEventsForHeroBackstory = builder.result())
|
||||
}
|
||||
finally rs.close()
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally eventsStmt.close()
|
||||
end try
|
||||
finally eventsStmt.close()
|
||||
end try
|
||||
}
|
||||
end if
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,27 +22,19 @@ import net.eagle0.eagle.model.proto_converters.ChangedProvinceConverter
|
||||
private[service] object ChangedProvinceTableReader {
|
||||
|
||||
/** Read every `ChangedProvince` for `actionSeq` from the v3 blob column, ordered by `n` (original vector order). */
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ChangedProvinceT] =
|
||||
readRange(connection, actionSeq, actionSeq + 1).getOrElse(actionSeq, Vector.empty)
|
||||
|
||||
def readRange(connection: Connection, startInclusive: Int, endExclusive: Int): Map[Int, Vector[ChangedProvinceT]] = {
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ChangedProvinceT] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, changed_province_proto
|
||||
| FROM action_changed_provinces
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
"SELECT changed_province_proto FROM action_changed_provinces WHERE action_seq = ? ORDER BY n"
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders = scala.collection.mutable.Map
|
||||
.empty[Int, scala.collection.mutable.Builder[ChangedProvinceT, Vector[ChangedProvinceT]]]
|
||||
ps.setInt(1, actionSeq)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[ChangedProvinceT]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += ChangedProvinceConverter.fromProto(
|
||||
builder += ChangedProvinceConverter.fromProto(
|
||||
ChangedProvinceProto.parseFrom(rs.getBytes("changed_province_proto"))
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
+23
-48
@@ -2,8 +2,6 @@ package net.eagle0.eagle.service
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
import net.eagle0.eagle.model.action_result.concrete.ClientTextVisibilityExtensionC
|
||||
import net.eagle0.eagle.model.action_result.ClientTextVisibilityExtensionT
|
||||
import net.eagle0.eagle.FactionId
|
||||
@@ -24,68 +22,45 @@ import net.eagle0.eagle.FactionId
|
||||
private[service] object ClientTextVisibilityExtensionTableReader {
|
||||
|
||||
/** Read every `clientTextVisibilityExtensions` element for `actionSeq`, ordered by `n` (original vector order). */
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ClientTextVisibilityExtensionT] =
|
||||
readRange(connection, actionSeq, actionSeq + 1).getOrElse(actionSeq, Vector.empty)
|
||||
|
||||
def readRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[ClientTextVisibilityExtensionT]] = {
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ClientTextVisibilityExtensionT] = {
|
||||
// Pass 1: parent rows (text_id per n). Drain + close before the per-parent child queries.
|
||||
val parents: Vector[(Int, Int, String)] = {
|
||||
val parents: Vector[(Int, String)] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, n, text_id
|
||||
| FROM action_client_text_visibility_extensions
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
"SELECT n, text_id FROM action_client_text_visibility_extensions WHERE action_seq = ? ORDER BY n"
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
ps.setInt(1, actionSeq)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[(Int, Int, String)]
|
||||
while rs.next() do builder += ((rs.getInt("action_seq"), rs.getInt("n"), rs.getString("text_id")))
|
||||
val builder = Vector.newBuilder[(Int, String)]
|
||||
while rs.next() do builder += ((rs.getInt("n"), rs.getString("text_id")))
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
// Pass 2: fill each parent's recipientFactionIds from the child table, keyed by (action_seq, n).
|
||||
val recipients = readRecipientsRange(connection, startInclusive, endExclusive)
|
||||
val builders = mutable.Map.empty[Int, scala.collection.mutable.Builder[ClientTextVisibilityExtensionT, Vector[
|
||||
ClientTextVisibilityExtensionT
|
||||
]]]
|
||||
parents.foreach {
|
||||
case (actionSeq, n, textId) =>
|
||||
builders.getOrElseUpdate(actionSeq, Vector.newBuilder) += ClientTextVisibilityExtensionC(
|
||||
textId = textId,
|
||||
recipientFactionIds = recipients.getOrElse((actionSeq, n), Vector.empty)
|
||||
)
|
||||
parents.map {
|
||||
case (n, textId) =>
|
||||
ClientTextVisibilityExtensionC(textId = textId, recipientFactionIds = readRecipients(connection, actionSeq, n))
|
||||
}
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
}
|
||||
|
||||
private def readRecipientsRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[(Int, Int), Vector[FactionId]] = {
|
||||
/**
|
||||
* Read all `faction_id`s for the given `(actionSeq, n)` from `action_client_text_visibility_extension_recipients`,
|
||||
* sorted ascending — set semantics, equivalent for callers that treat `recipientFactionIds` as the set "who can see
|
||||
* this text".
|
||||
*/
|
||||
private def readRecipients(connection: Connection, actionSeq: Int, n: Int): Vector[FactionId] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, n, faction_id
|
||||
| FROM action_client_text_visibility_extension_recipients
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n, faction_id""".stripMargin
|
||||
"SELECT faction_id FROM action_client_text_visibility_extension_recipients " +
|
||||
"WHERE action_seq = ? AND n = ? ORDER BY faction_id"
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders = mutable.Map.empty[(Int, Int), scala.collection.mutable.Builder[FactionId, Vector[FactionId]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate((rs.getInt("action_seq"), rs.getInt("n")), Vector.newBuilder) += rs.getInt(
|
||||
"faction_id"
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
ps.setInt(1, actionSeq)
|
||||
ps.setInt(2, n)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[FactionId]
|
||||
while rs.next() do builder += rs.getInt("faction_id")
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package net.eagle0.eagle.service
|
||||
import java.io.File
|
||||
|
||||
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
|
||||
import net.eagle0.eagle.library.EagleInternalException
|
||||
import net.eagle0.eagle.model.action_result.ActionResultT
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.service.persistence.{Persister, SaveDirectory}
|
||||
@@ -51,20 +50,8 @@ private[service] object GameHistoryFactory {
|
||||
else None
|
||||
}
|
||||
|
||||
if HistoryBackendConfig.usePostgres then {
|
||||
PostgresHistory.requireConfigured()
|
||||
val postgres = PostgresHistory.loaded(gameId)
|
||||
if postgres.isEmpty then
|
||||
println(
|
||||
s"GameHistoryFactory: no Postgres history found for game ${gameId.toHexString}; trying SQLite fallback"
|
||||
)
|
||||
val fallback = postgres.orElse(sqlite)
|
||||
if fallback.isEmpty then
|
||||
throw new EagleInternalException(
|
||||
s"No history found for game ${gameId.toHexString}: Postgres schema missing and SQLite game.db missing"
|
||||
)
|
||||
fallback
|
||||
} else sqlite
|
||||
if HistoryBackendConfig.usePostgres then PostgresHistory.loaded(gameId).orElse(sqlite)
|
||||
else sqlite
|
||||
}
|
||||
|
||||
private def sqliteDbFile(gameId: GameId): File =
|
||||
|
||||
@@ -9,7 +9,7 @@ import io.sentry.Sentry
|
||||
import net.eagle0.common.{ProtoParser, SeededRandom, SimpleTimedLogger, ZipUtils}
|
||||
import net.eagle0.common.shardok_internal_interface.ShardokInternalInterfaceGrpc.ShardokInternalInterfaceStub
|
||||
import net.eagle0.common.tutorial_battle_config.TutorialBattleConfig
|
||||
import net.eagle0.eagle.{ClientTextId, FactionId, GameId, ProvinceId, ShardokGameId, UserId}
|
||||
import net.eagle0.eagle.{FactionId, GameId, ProvinceId, ShardokGameId, UserId}
|
||||
import net.eagle0.eagle.admin.game_admin.{ConvertAiToHumanResponse, ConvertHumanToAiResponse, ReassignFactionResponse}
|
||||
import net.eagle0.eagle.api.eagle.*
|
||||
import net.eagle0.eagle.api.eagle.JoinGameResult.INVALID_OPTIONS_RESULT
|
||||
@@ -19,7 +19,6 @@ import net.eagle0.eagle.api.selected_command.SelectedCommand
|
||||
import net.eagle0.eagle.auth.UserService
|
||||
import net.eagle0.eagle.client_text.{
|
||||
ClientTextStore,
|
||||
PostgresClientTextStore,
|
||||
PregeneratedClientTextStore,
|
||||
SqliteClientTextStore,
|
||||
TextGenerationSuccess,
|
||||
@@ -225,80 +224,6 @@ object GamesManager {
|
||||
)
|
||||
}
|
||||
|
||||
private def loadedClientTextStore(
|
||||
gameId: GameId,
|
||||
databasePersister: Option[Persister],
|
||||
pregenerated: PregeneratedClientTextStore
|
||||
): ClientTextStore =
|
||||
if HistoryBackendConfig.usePostgres then {
|
||||
val postgresStore = PostgresClientTextStore
|
||||
.loaded(
|
||||
gameId = gameId,
|
||||
pregenerated = pregenerated
|
||||
)
|
||||
.get
|
||||
importLocalSqliteTextStoreIfNeeded(
|
||||
gameId = gameId,
|
||||
postgresStore = postgresStore,
|
||||
databasePersister = databasePersister,
|
||||
pregenerated = pregenerated
|
||||
)
|
||||
postgresStore
|
||||
} else
|
||||
SqliteClientTextStore
|
||||
.loaded(
|
||||
saveDirectory = new java.io.File(SaveDirectory.saveDirectoryForGame(gameId)),
|
||||
pregenerated = pregenerated,
|
||||
persister = databasePersister
|
||||
)
|
||||
.get
|
||||
|
||||
private def importLocalSqliteTextStoreIfNeeded(
|
||||
gameId: GameId,
|
||||
postgresStore: PostgresClientTextStore,
|
||||
databasePersister: Option[Persister],
|
||||
pregenerated: PregeneratedClientTextStore
|
||||
): Unit = {
|
||||
val saveDirectory = new java.io.File(SaveDirectory.saveDirectoryForGame(gameId))
|
||||
val sqliteDbFile = new java.io.File(saveDirectory, "text_store.db")
|
||||
if !postgresStore.hasAnyTexts && sqliteDbFile.exists() then {
|
||||
val sqliteStore = SqliteClientTextStore
|
||||
.loaded(
|
||||
saveDirectory = saveDirectory,
|
||||
pregenerated = pregenerated,
|
||||
persister = databasePersister
|
||||
)
|
||||
.get
|
||||
postgresStore.importFrom(sqliteStore)
|
||||
println(
|
||||
s"Imported local text_store.db into Postgres text store for game ${gameId.toHexString}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def createClientTextStoreWithData(
|
||||
gameId: GameId,
|
||||
pregenerated: PregeneratedClientTextStore,
|
||||
unrequestedTexts: Map[ClientTextId, UnrequestedClientText],
|
||||
accessibleTo: Map[ClientTextId, Vector[FactionId]],
|
||||
databasePersister: Option[Persister]
|
||||
): ClientTextStore =
|
||||
if HistoryBackendConfig.usePostgres then
|
||||
PostgresClientTextStore.createWithData(
|
||||
gameId = gameId,
|
||||
pregenerated = pregenerated,
|
||||
unrequestedTexts = unrequestedTexts,
|
||||
accessibleTo = accessibleTo
|
||||
)
|
||||
else
|
||||
SqliteClientTextStore.createWithData(
|
||||
dbPath = new java.io.File(SaveDirectory.saveDirectoryForGame(gameId), "text_store.db"),
|
||||
pregenerated = pregenerated,
|
||||
unrequestedTexts = unrequestedTexts,
|
||||
accessibleTo = accessibleTo,
|
||||
persister = databasePersister
|
||||
)
|
||||
|
||||
private def gameController(
|
||||
gameId: GameId,
|
||||
databasePersister: Option[Persister],
|
||||
@@ -307,11 +232,13 @@ object GamesManager {
|
||||
pregeneratedHeroes: Vector[HeroWithName]
|
||||
): GameController = {
|
||||
val textStoreStart = System.currentTimeMillis()
|
||||
val loadedTextStore = GamesManager.loadedClientTextStore(
|
||||
gameId = gameId,
|
||||
databasePersister = databasePersister,
|
||||
pregenerated = pregeneratedTexts
|
||||
)
|
||||
val loadedTextStore = SqliteClientTextStore
|
||||
.loaded(
|
||||
saveDirectory = new java.io.File(SaveDirectory.saveDirectoryForGame(gameId)),
|
||||
pregenerated = pregeneratedTexts,
|
||||
persister = databasePersister
|
||||
)
|
||||
.get
|
||||
val textStoreDuration = System.currentTimeMillis() - textStoreStart
|
||||
|
||||
// Check all heroes' nameTextIds and recover any missing name requests
|
||||
@@ -1613,12 +1540,16 @@ class GamesManager(
|
||||
if startPhase == TutorialPhase.OpeningBattle then
|
||||
tutorialBattleConfigs = tutorialBattleConfigs + (gameId -> tutorialConfig)
|
||||
|
||||
val initialTextStore = GamesManager.createClientTextStoreWithData(
|
||||
gameId = gameId,
|
||||
// Use the SQLite-backed store, matching normal game creation. The reload path
|
||||
// (gameController -> SqliteClientTextStore.loaded) always reads text_store.db, so the
|
||||
// tutorial must persist there too — otherwise texts generated mid-tutorial (e.g. yearly
|
||||
// chronicle updates) are lost on the first save+reload.
|
||||
val initialTextStore = SqliteClientTextStore.createWithData(
|
||||
dbPath = new java.io.File(SaveDirectory.saveDirectoryForGame(gameId), "text_store.db"),
|
||||
pregenerated = pregeneratedClientText,
|
||||
unrequestedTexts = Map.empty,
|
||||
accessibleTo = Map.empty,
|
||||
databasePersister = gamePersisterCreation.databaseUploadPersisterForGame(gameId)
|
||||
persister = gamePersisterCreation.databaseUploadPersisterForGame(gameId)
|
||||
)
|
||||
|
||||
// Copy pregenerated texts (hero names, backstories) into the game's text store
|
||||
@@ -1753,16 +1684,16 @@ class GamesManager(
|
||||
.toMap
|
||||
|
||||
// Create the text store and populate it with pregenerated texts for heroes in this game
|
||||
val initialTextStore = logger.withStopwatch(s"[CREATE:$gameIdHex] createClientTextStoreWithData") {
|
||||
GamesManager.createClientTextStoreWithData(
|
||||
gameId = gameId,
|
||||
val initialTextStore = logger.withStopwatch(s"[CREATE:$gameIdHex] SqliteClientTextStore.createWithData") {
|
||||
SqliteClientTextStore.createWithData(
|
||||
dbPath = new java.io.File(SaveDirectory.saveDirectoryForGame(gameId), "text_store.db"),
|
||||
pregenerated = pregeneratedClientText,
|
||||
unrequestedTexts = unrequestedTexts,
|
||||
// FIXME: this is making it all accessible to everyone
|
||||
accessibleTo = unrequestedTexts.values
|
||||
.map(ut => ut.id -> initialEar.engine.factionIds)
|
||||
.toMap,
|
||||
databasePersister = gamePersisterCreation.databaseUploadPersisterForGame(gameId)
|
||||
persister = gamePersisterCreation.databaseUploadPersisterForGame(gameId)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,31 +20,19 @@ import net.eagle0.eagle.model.proto_converters.GeneratedTextRequestConverter
|
||||
private[service] object GeneratedTextRequestTableReader {
|
||||
|
||||
/** Read every new generated text request for `actionSeq`, ordered by `n` (original vector order). */
|
||||
def read(connection: Connection, actionSeq: Int): Vector[GeneratedTextRequestT] =
|
||||
readRange(connection, actionSeq, actionSeq + 1).getOrElse(actionSeq, Vector.empty)
|
||||
|
||||
def readRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[GeneratedTextRequestT]] = {
|
||||
def read(connection: Connection, actionSeq: Int): Vector[GeneratedTextRequestT] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, generated_text_request_proto
|
||||
| FROM action_new_generated_text_requests
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
"SELECT generated_text_request_proto FROM action_new_generated_text_requests WHERE action_seq = ? ORDER BY n"
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders = scala.collection.mutable.Map
|
||||
.empty[Int, scala.collection.mutable.Builder[GeneratedTextRequestT, Vector[GeneratedTextRequestT]]]
|
||||
ps.setInt(1, actionSeq)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[GeneratedTextRequestT]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += GeneratedTextRequestConverter.fromProto(
|
||||
builder += GeneratedTextRequestConverter.fromProto(
|
||||
GeneratedTextRequestProto.parseFrom(rs.getBytes("generated_text_request_proto"))
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ private[service] object HistoryBackendConfig {
|
||||
.get(HistoryBackendEnv)
|
||||
.map(_.trim.toLowerCase)
|
||||
.filter(_.nonEmpty)
|
||||
.getOrElse(PostgresValue)
|
||||
.getOrElse(SqliteValue)
|
||||
else settingValue
|
||||
}
|
||||
|
||||
|
||||
@@ -18,27 +18,17 @@ import net.eagle0.eagle.model.state.battalion.BattalionT
|
||||
private[service] object NewBattalionTableReader {
|
||||
|
||||
/** Read every new battalion for `actionSeq`, ordered by `n` (original vector order). */
|
||||
def read(connection: Connection, actionSeq: Int): Vector[BattalionT] =
|
||||
readRange(connection, actionSeq, actionSeq + 1).getOrElse(actionSeq, Vector.empty)
|
||||
|
||||
def readRange(connection: Connection, startInclusive: Int, endExclusive: Int): Map[Int, Vector[BattalionT]] = {
|
||||
def read(connection: Connection, actionSeq: Int): Vector[BattalionT] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, battalion_proto
|
||||
| FROM action_new_battalions
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
"SELECT battalion_proto FROM action_new_battalions WHERE action_seq = ? ORDER BY n"
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders =
|
||||
scala.collection.mutable.Map.empty[Int, scala.collection.mutable.Builder[BattalionT, Vector[BattalionT]]]
|
||||
ps.setInt(1, actionSeq)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[BattalionT]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += BattalionConverter.fromProto(
|
||||
BattalionProto.parseFrom(rs.getBytes("battalion_proto"))
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
builder += BattalionConverter.fromProto(BattalionProto.parseFrom(rs.getBytes("battalion_proto")))
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,27 +18,16 @@ import net.eagle0.eagle.model.state.faction.FactionT
|
||||
private[service] object NewFactionTableReader {
|
||||
|
||||
/** Read every new faction for `actionSeq`, ordered by `n` (original vector order). */
|
||||
def read(connection: Connection, actionSeq: Int): Vector[FactionT] =
|
||||
readRange(connection, actionSeq, actionSeq + 1).getOrElse(actionSeq, Vector.empty)
|
||||
|
||||
def readRange(connection: Connection, startInclusive: Int, endExclusive: Int): Map[Int, Vector[FactionT]] = {
|
||||
def read(connection: Connection, actionSeq: Int): Vector[FactionT] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, faction_proto
|
||||
| FROM action_new_factions
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
"SELECT faction_proto FROM action_new_factions WHERE action_seq = ? ORDER BY n"
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders =
|
||||
scala.collection.mutable.Map.empty[Int, scala.collection.mutable.Builder[FactionT, Vector[FactionT]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += FactionConverter.fromProto(
|
||||
FactionProto.parseFrom(rs.getBytes("faction_proto"))
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
ps.setInt(1, actionSeq)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[FactionT]
|
||||
while rs.next() do builder += FactionConverter.fromProto(FactionProto.parseFrom(rs.getBytes("faction_proto")))
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,26 +18,16 @@ import net.eagle0.eagle.model.state.hero.HeroT
|
||||
private[service] object NewHeroTableReader {
|
||||
|
||||
/** Read every new hero for `actionSeq`, ordered by `n` (original vector order). */
|
||||
def read(connection: Connection, actionSeq: Int): Vector[HeroT] =
|
||||
readRange(connection, actionSeq, actionSeq + 1).getOrElse(actionSeq, Vector.empty)
|
||||
|
||||
def readRange(connection: Connection, startInclusive: Int, endExclusive: Int): Map[Int, Vector[HeroT]] = {
|
||||
def read(connection: Connection, actionSeq: Int): Vector[HeroT] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, hero_proto
|
||||
| FROM action_new_heroes
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
"SELECT hero_proto FROM action_new_heroes WHERE action_seq = ? ORDER BY n"
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders = scala.collection.mutable.Map.empty[Int, scala.collection.mutable.Builder[HeroT, Vector[HeroT]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += HeroConverter.fromProto(
|
||||
HeroProto.parseFrom(rs.getBytes("hero_proto"))
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
ps.setInt(1, actionSeq)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[HeroT]
|
||||
while rs.next() do builder += HeroConverter.fromProto(HeroProto.parseFrom(rs.getBytes("hero_proto")))
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,27 +18,16 @@ import net.eagle0.eagle.model.state.province.ProvinceT
|
||||
private[service] object NewProvinceTableReader {
|
||||
|
||||
/** Read every new province for `actionSeq`, ordered by `n` (original vector order). */
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ProvinceT] =
|
||||
readRange(connection, actionSeq, actionSeq + 1).getOrElse(actionSeq, Vector.empty)
|
||||
|
||||
def readRange(connection: Connection, startInclusive: Int, endExclusive: Int): Map[Int, Vector[ProvinceT]] = {
|
||||
def read(connection: Connection, actionSeq: Int): Vector[ProvinceT] = {
|
||||
val ps = connection.prepareStatement(
|
||||
"""SELECT action_seq, province_proto
|
||||
| FROM action_new_provinces
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
"SELECT province_proto FROM action_new_provinces WHERE action_seq = ? ORDER BY n"
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders =
|
||||
scala.collection.mutable.Map.empty[Int, scala.collection.mutable.Builder[ProvinceT, Vector[ProvinceT]]]
|
||||
while rs.next() do
|
||||
builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += ProvinceConverter.fromProto(
|
||||
ProvinceProto.parseFrom(rs.getBytes("province_proto"))
|
||||
)
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
ps.setInt(1, actionSeq)
|
||||
val rs = ps.executeQuery()
|
||||
val builder = Vector.newBuilder[ProvinceT]
|
||||
while rs.next() do builder += ProvinceConverter.fromProto(ProvinceProto.parseFrom(rs.getBytes("province_proto")))
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,20 +22,10 @@ private[service] object NotificationTableReader {
|
||||
def readNew(connection: Connection, actionSeq: Int): Vector[NotificationT] =
|
||||
read(connection, "action_new_notifications", actionSeq)
|
||||
|
||||
def readNewRange(connection: Connection, startInclusive: Int, endExclusive: Int): Map[Int, Vector[NotificationT]] =
|
||||
readRange(connection, "action_new_notifications", startInclusive, endExclusive)
|
||||
|
||||
/** Read every `removedNotifications` element for `actionSeq`, ordered by `n`. */
|
||||
def readRemoved(connection: Connection, actionSeq: Int): Vector[NotificationT] =
|
||||
read(connection, "action_removed_notifications", actionSeq)
|
||||
|
||||
def readRemovedRange(
|
||||
connection: Connection,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[NotificationT]] =
|
||||
readRange(connection, "action_removed_notifications", startInclusive, endExclusive)
|
||||
|
||||
/** Shared SELECT for both notification tables (`table` is an internal constant, never user input). */
|
||||
private def read(connection: Connection, table: String, actionSeq: Int): Vector[NotificationT] = {
|
||||
val ps = connection.prepareStatement(
|
||||
@@ -52,34 +42,4 @@ private[service] object NotificationTableReader {
|
||||
builder.result()
|
||||
} finally ps.close()
|
||||
}
|
||||
|
||||
private def readRange(
|
||||
connection: Connection,
|
||||
table: String,
|
||||
startInclusive: Int,
|
||||
endExclusive: Int
|
||||
): Map[Int, Vector[NotificationT]] = {
|
||||
val ps = connection.prepareStatement(
|
||||
s"""SELECT action_seq, deferred, notification_proto
|
||||
| FROM $table
|
||||
| WHERE action_seq >= ? AND action_seq < ?
|
||||
| ORDER BY action_seq, n""".stripMargin
|
||||
)
|
||||
try {
|
||||
ps.setInt(1, startInclusive)
|
||||
ps.setInt(2, endExclusive)
|
||||
val rs = ps.executeQuery()
|
||||
val builders =
|
||||
scala.collection.mutable.Map.empty[Int, scala.collection.mutable.Builder[NotificationT, Vector[NotificationT]]]
|
||||
while rs.next() do {
|
||||
val proto = NotificationProto.parseFrom(rs.getBytes("notification_proto"))
|
||||
builders.getOrElseUpdate(rs.getInt("action_seq"), Vector.newBuilder) += NotificationConverter.fromProto(
|
||||
proto,
|
||||
deferred = rs.getInt("deferred") != 0
|
||||
)
|
||||
}
|
||||
builders.view.mapValues(_.result()).toMap
|
||||
} finally ps.close()
|
||||
end try
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,6 @@ class PostgresHistory private[service] (
|
||||
}
|
||||
|
||||
def allActionResults: Vector[ActionResultT] = withDbLock {
|
||||
prefetchActionResults(0, cachedCount)
|
||||
(0 until cachedCount).map(readActionResult).toVector
|
||||
}
|
||||
|
||||
@@ -234,19 +233,11 @@ class PostgresHistory private[service] (
|
||||
private def readActionResult(seq: Int): ActionResultT =
|
||||
actionCache.getOrElseUpdate(seq, ActionResultDbReader.readPostgres(connection, seq))
|
||||
|
||||
private def prefetchActionResults(startInclusive: Int, endExclusive: Int): Unit =
|
||||
if startInclusive < endExclusive && (startInclusive until endExclusive).exists(seq => !actionCache.contains(seq))
|
||||
then
|
||||
ActionResultDbReader.readRangePostgres(connection, startInclusive, endExclusive).foreach {
|
||||
case (seq, actionResult) => actionCache(seq) = actionResult
|
||||
}
|
||||
|
||||
private def bestCachedStateAtOrBefore(count: Int): Option[(Int, GameState)] =
|
||||
stateCache.keysIterator.filter(_ <= count).maxOption.map(boundary => boundary -> stateCache(boundary))
|
||||
|
||||
private def replayStateFrom(startBoundary: Int, startState: GameState, endExclusiveSeq: Int): GameState = {
|
||||
var state = startState
|
||||
prefetchActionResults(startBoundary, endExclusiveSeq)
|
||||
readActionSeqs(startBoundary, endExclusiveSeq).foreach { seq =>
|
||||
state = applier.applyActionResult(state, readActionResult(seq)).resultingState
|
||||
stateCache(seq + 1) = state
|
||||
@@ -509,7 +500,6 @@ class PostgresHistory private[service] (
|
||||
event.snapshotHit = false
|
||||
var state = startState
|
||||
val builder = Vector.newBuilder[ActionResultWithResultingState]
|
||||
prefetchActionResults(startBoundary, endExclusiveSeq)
|
||||
readActionSeqs(startBoundary, endExclusiveSeq).foreach { seq =>
|
||||
val ar = readActionResult(seq)
|
||||
state = applier.applyActionResult(state, ar).resultingState
|
||||
@@ -614,9 +604,6 @@ object PostgresHistory {
|
||||
try {
|
||||
val schemaName = schemaNameForGame(gameId)
|
||||
if !schemaExists(connection, schemaName) then {
|
||||
println(
|
||||
s"PostgresHistory: no schema $schemaName for game ${gameId.toHexString}"
|
||||
)
|
||||
connection.close()
|
||||
None
|
||||
} else {
|
||||
@@ -631,13 +618,6 @@ object PostgresHistory {
|
||||
}
|
||||
}.getOrElse(None)
|
||||
|
||||
def requireConfigured(): Unit =
|
||||
if config.isEmpty then
|
||||
throw new EagleInternalException(
|
||||
"HistoryBackend=postgres requires EAGLE_POSTGRES_HOST, EAGLE_POSTGRES_DATABASE, " +
|
||||
"EAGLE_POSTGRES_USER, and EAGLE_POSTGRES_PASSWORD"
|
||||
)
|
||||
|
||||
private def config: Option[PostgresConfig] =
|
||||
for
|
||||
host <- envNonEmpty("EAGLE_POSTGRES_HOST")
|
||||
|
||||
@@ -151,7 +151,6 @@ scala_test(
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:sqlite_client_text_store",
|
||||
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
|
||||
"//src/main/scala/net/eagle0/eagle/library:engine",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:history_backend",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state/hero/concrete",
|
||||
|
||||
@@ -16,7 +16,6 @@ import net.eagle0.eagle.auth.NoOpUserService
|
||||
import net.eagle0.eagle.client_text.{PregeneratedClientTextStore, SqliteClientTextStore}
|
||||
import net.eagle0.eagle.internal.game_parameters.GameParameters
|
||||
import net.eagle0.eagle.library.{Engine, EngineAndResults}
|
||||
import net.eagle0.eagle.library.settings.HistoryBackend
|
||||
import net.eagle0.eagle.model.state.{GameType, RoundPhase}
|
||||
import net.eagle0.eagle.model.state.game_state.GameState
|
||||
import net.eagle0.eagle.model.state.run_status.RunStatus
|
||||
@@ -75,7 +74,6 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
private val mockEngine = mock[Engine]
|
||||
private val mockHistory = mock[FullGameHistory]
|
||||
private val mockPersister = mock[Persister]
|
||||
private var previousHistoryBackend = "env"
|
||||
|
||||
private val greatPerson1 =
|
||||
GreatPersonWithBackstory(
|
||||
@@ -111,15 +109,10 @@ class GamesManagerTest extends AnyFlatSpec with MockFactory with Matchers with B
|
||||
|
||||
val dir = new File("/tmp/eagle0")
|
||||
|
||||
override def beforeEach(): Unit = {
|
||||
val _ = GamesManager.instanceStartTimeMs
|
||||
previousHistoryBackend = HistoryBackend.stringValue
|
||||
HistoryBackend.setStringValue("sqlite")
|
||||
override def beforeEach(): Unit =
|
||||
mockRandom.reset()
|
||||
}
|
||||
|
||||
override def afterEach(): Unit = {
|
||||
HistoryBackend.setStringValue(previousHistoryBackend)
|
||||
// Clean up SQLite files created during tests
|
||||
val saveDir = new File(SaveDirectory.saveDirectoryForGame(gameId))
|
||||
if saveDir.exists() then {
|
||||
|
||||
@@ -2146,47 +2146,4 @@ class SqliteHistoryTest extends AnyFlatSpec with Matchers with BeforeAndAfterEac
|
||||
} finally stmt.close()
|
||||
}
|
||||
}
|
||||
|
||||
"ActionResultDbReader.readRangePostgres" should
|
||||
"reconstruct the same action results as single-row reads across a range" in {
|
||||
val dense = ActionResultC(
|
||||
actionResultType = ActionResultType.UnknownActionUnspecified,
|
||||
newDate = Some(Date(year = 1501, month = Date.Month.January)),
|
||||
changedBattalions = Vector(ChangedBattalionC(battalion(id = 1, name = "Vanguard"))),
|
||||
changedFactions = Vector(ChangedFactionC(factionId = 3)),
|
||||
changedHeroes = Vector(ChangedHeroC(heroId = 7)),
|
||||
changedProvinces = Vector(ChangedProvinceC(provinceId = 11)),
|
||||
newBattalions = Vector(battalion(id = 21, name = "Reserve")),
|
||||
newFactions = Vector(faction(id = 31, name = "Crown")),
|
||||
newHeroes = Vector(hero(id = 41, nameTextId = "name-41")),
|
||||
newProvinces = Vector(province(id = 51, name = "Eastmarch")),
|
||||
newBattalionTypes = Vector(sampleBattalionType),
|
||||
destroyedBattalionIds = Vector(61),
|
||||
removedHeroIds = Vector(71),
|
||||
removedFactionIds = Vector(81),
|
||||
clientTextVisibilityExtensions =
|
||||
Vector(clientTextVisibilityExtension(textId = "text-a", recipientFactionIds = Vector(3))),
|
||||
newGeneratedTextRequests = Vector(generatedHeroName(requestId = "req-1")),
|
||||
newNotifications = Vector(notification(NotificationDetails.Divined, deferred = false)),
|
||||
removedNotifications = Vector(notification(heroDepartureDetails, deferred = false)),
|
||||
affectedFactionIds = Vector(91)
|
||||
)
|
||||
val sparse = makeActionResult(1501, 2, 2L)
|
||||
val history = SqliteHistory.create(dbFile, emptyStartingState)
|
||||
history
|
||||
.withNewResults(
|
||||
Vector(
|
||||
ActionResultWithResultingState(dense, emptyStartingState),
|
||||
ActionResultWithResultingState(sparse, emptyStartingState)
|
||||
)
|
||||
)
|
||||
.close()
|
||||
|
||||
withRawConnection { conn =>
|
||||
ActionResultDbReader.readRangePostgres(conn, startInclusive = 0, endExclusive = 2) shouldBe Vector(
|
||||
0 -> ActionResultDbReader.read(conn, actionSeq = 0),
|
||||
1 -> ActionResultDbReader.read(conn, actionSeq = 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ scala_test(
|
||||
deps = [
|
||||
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_scala_proto",
|
||||
"//src/main/scala/net/eagle0/eagle/library:engine",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:history_backend",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings/loaders:battalion_type_loader",
|
||||
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
|
||||
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:game_parameters_utils",
|
||||
|
||||
-2
@@ -7,7 +7,6 @@ import scala.util.{Failure, Try}
|
||||
|
||||
import net.eagle0.common.tutorial_battle_config.TutorialTrigger
|
||||
import net.eagle0.eagle.library.settings.loaders.BattalionTypeLoader
|
||||
import net.eagle0.eagle.library.settings.HistoryBackend
|
||||
import net.eagle0.eagle.service.persistence.Persister
|
||||
import net.eagle0.util.hero_generation.LoadedHero
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
@@ -20,7 +19,6 @@ class TutorialGameCreationTest extends AnyFlatSpec with Matchers {
|
||||
// sandbox-writable temp directory. SaveDirectory reads the system property on first access (it's a `val`), so this
|
||||
// must run before any code touches SaveDirectory.
|
||||
System.setProperty("eagle.save.dir", Files.createTempDirectory("tutorial_game_creation_test").toString)
|
||||
HistoryBackend.setStringValue("sqlite")
|
||||
|
||||
// No-op persister for testing - we don't need actual persistence
|
||||
private object NoOpPersister extends Persister {
|
||||
|
||||
@@ -61,7 +61,6 @@ scala_test(
|
||||
deps = [
|
||||
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
|
||||
"//src/main/scala/net/eagle0/eagle/library:engine",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings:history_backend",
|
||||
"//src/main/scala/net/eagle0/eagle/library/settings/loaders:battalion_type_loader",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
|
||||
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
|
||||
|
||||
@@ -6,7 +6,6 @@ import java.nio.file.Files
|
||||
import scala.util.{Failure, Try}
|
||||
|
||||
import net.eagle0.eagle.library.settings.loaders.BattalionTypeLoader
|
||||
import net.eagle0.eagle.library.settings.HistoryBackend
|
||||
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
|
||||
import net.eagle0.eagle.model.action_result.concrete.{ActionResultC, ChangedHeroC, StatAbsolute}
|
||||
import net.eagle0.eagle.model.state.battalion.concrete.BattalionC
|
||||
@@ -29,7 +28,6 @@ class EagleAppearsActionTest extends AnyFlatSpec with Matchers {
|
||||
// See note in TutorialGameCreationTest — SqliteHistory writes game.db under SaveDirectory.saveDirectoryForGame, so
|
||||
// point it at a sandbox-writable temp directory before any code touches SaveDirectory.
|
||||
System.setProperty("eagle.save.dir", Files.createTempDirectory("eagle_appears_action_test").toString)
|
||||
HistoryBackend.setStringValue("sqlite")
|
||||
|
||||
private object NoOpPersister extends Persister {
|
||||
override def save(key: String, bytes: Array[Byte]): Boolean = true
|
||||
|
||||
Reference in New Issue
Block a user