Wire replica sync into client target shell
This commit is contained in:
parent
9a52ac5767
commit
a04df8a585
9 changed files with 312 additions and 6 deletions
|
|
@ -17,6 +17,7 @@
|
|||
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeVerseDiscovery.cs" Link="AetheriaRuntimeVerseDiscovery.cs" />
|
||||
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateBoundary.cs" Link="AetheriaRuntimeStateBoundary.cs" />
|
||||
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateBoot.cs" Link="AetheriaRuntimeStateBoot.cs" />
|
||||
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeVerseReplicaBridge.cs" Link="AetheriaRuntimeVerseReplicaBridge.cs" />
|
||||
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeStateReader.cs" Link="AetheriaRuntimeStateReader.cs" />
|
||||
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeBehaviorMetadata.cs" Link="AetheriaRuntimeBehaviorMetadata.cs" />
|
||||
<Compile Include="..\Packages\org.gamecult.aetheria.state\Runtime\AetheriaRuntimeBehaviorKinds.cs" Link="AetheriaRuntimeBehaviorKinds.cs" />
|
||||
|
|
|
|||
|
|
@ -3878,6 +3878,7 @@ static void RequireClientTargetBootAuthority(string root)
|
|||
var bootPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeStateBoot.cs");
|
||||
var clientTargetStorePath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeClientTargetStore.cs");
|
||||
var verseDiscoveryPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeVerseDiscovery.cs");
|
||||
var replicaBridgePath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeVerseReplicaBridge.cs");
|
||||
var bootstrapPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.eve-runtime", "Runtime", "AetheriaEveRuntimeBootstrap.cs");
|
||||
var presenterPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.eve-runtime", "Runtime", "AetheriaEveSurfacePresenter.cs");
|
||||
var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs");
|
||||
|
|
@ -3890,6 +3891,7 @@ static void RequireClientTargetBootAuthority(string root)
|
|||
bootPath,
|
||||
clientTargetStorePath,
|
||||
verseDiscoveryPath,
|
||||
replicaBridgePath,
|
||||
bootstrapPath,
|
||||
presenterPath,
|
||||
actionGameManagerPath,
|
||||
|
|
@ -3912,6 +3914,7 @@ static void RequireClientTargetBootAuthority(string root)
|
|||
var boot = File.ReadAllText(bootPath);
|
||||
var clientTargetStore = File.ReadAllText(clientTargetStorePath);
|
||||
var verseDiscovery = File.ReadAllText(verseDiscoveryPath);
|
||||
var replicaBridge = File.ReadAllText(replicaBridgePath);
|
||||
var bootstrap = File.ReadAllText(bootstrapPath);
|
||||
var presenter = File.ReadAllText(presenterPath);
|
||||
var actionGameManager = File.ReadAllText(actionGameManagerPath);
|
||||
|
|
@ -3922,7 +3925,8 @@ static void RequireClientTargetBootAuthority(string root)
|
|||
"AetheriaRuntimeClientTargetStore.cs",
|
||||
"AetheriaRuntimeVerseDiscovery.cs",
|
||||
"AetheriaRuntimeStateBoundary.cs",
|
||||
"AetheriaRuntimeStateBoot.cs"
|
||||
"AetheriaRuntimeStateBoot.cs",
|
||||
"AetheriaRuntimeVerseReplicaBridge.cs"
|
||||
};
|
||||
var missingUnityFacadeSymbols = requiredUnityFacadeSymbols
|
||||
.Where(symbol => !unityFacadeProject.Contains(symbol, StringComparison.Ordinal))
|
||||
|
|
@ -3967,6 +3971,8 @@ static void RequireClientTargetBootAuthority(string root)
|
|||
"LastDiscoveryAtUtc",
|
||||
"LastDiscoveryError",
|
||||
"ReplicaStateFilePath",
|
||||
"LastReplicaSyncAtUtc",
|
||||
"LastReplicaSyncError",
|
||||
"ReadOrInitialize",
|
||||
"CreateDefault",
|
||||
"Write(string clientTargetPath, AetheriaRuntimeClientTargetDocument document)",
|
||||
|
|
@ -3994,6 +4000,8 @@ static void RequireClientTargetBootAuthority(string root)
|
|||
"LastDiscoveryAtUtc",
|
||||
"LastDiscoveryError",
|
||||
"ReplicaStateFilePath",
|
||||
"LastReplicaSyncAtUtc",
|
||||
"LastReplicaSyncError",
|
||||
"TargetLabel",
|
||||
"AetheriaRuntimeClientTargetStore.ReadOrInitialize",
|
||||
"AetheriaRuntimeStateBoundary.ResolveStatePathOverride()",
|
||||
|
|
@ -4032,6 +4040,25 @@ static void RequireClientTargetBootAuthority(string root)
|
|||
string.Join(", ", missingVerseDiscoverySymbols));
|
||||
}
|
||||
|
||||
var requiredReplicaBridgeSymbols = new[]
|
||||
{
|
||||
"public static class AetheriaRuntimeVerseReplicaBridge",
|
||||
"AetheriaRuntimeVerseReplicaSyncResult",
|
||||
"Aetheria.State.Replica",
|
||||
"Sync(",
|
||||
"ProcessStartInfo",
|
||||
"AetheriaRuntimeStateBoundary.GetReplicaStateFilePath"
|
||||
};
|
||||
var missingReplicaBridgeSymbols = requiredReplicaBridgeSymbols
|
||||
.Where(symbol => !replicaBridge.Contains(symbol, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (missingReplicaBridgeSymbols.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Runtime Verse replica bridge is incomplete: " +
|
||||
string.Join(", ", missingReplicaBridgeSymbols));
|
||||
}
|
||||
|
||||
var requiredActionGameManagerSymbols = new[]
|
||||
{
|
||||
"AetheriaRuntimeStateBoot.Inspect(GameDataDirectory)",
|
||||
|
|
@ -4082,6 +4109,8 @@ static void RequireClientTargetBootAuthority(string root)
|
|||
"stateBoot.LastDiscoveryAtUtc",
|
||||
"stateBoot.LastDiscoveryError",
|
||||
"stateBoot.ReplicaStateFilePath",
|
||||
"stateBoot.LastReplicaSyncAtUtc",
|
||||
"stateBoot.LastReplicaSyncError",
|
||||
"\"Client Target\"",
|
||||
"\"Transport\"",
|
||||
"\"Target Source\""
|
||||
|
|
@ -4232,6 +4261,7 @@ static void RequireVerseSettingsShellAndBridge(string root)
|
|||
var verseHostCommandsPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeVerseHostCommands.cs");
|
||||
var clientTargetStorePath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeClientTargetStore.cs");
|
||||
var verseDiscoveryPath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeVerseDiscovery.cs");
|
||||
var replicaBridgePath = Path.Combine(root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeVerseReplicaBridge.cs");
|
||||
var mainMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "MainMenu.cs");
|
||||
var commandBridgePath = Path.Combine(root, "Aetheria.State", "AetheriaEveCommandBridge.cs");
|
||||
var drainStatusPath = Path.Combine(root, "Aetheria.State", "Documents", "AetheriaEveCommandDrainStatus.cs");
|
||||
|
|
@ -4248,6 +4278,7 @@ static void RequireVerseSettingsShellAndBridge(string root)
|
|||
verseHostCommandsPath,
|
||||
clientTargetStorePath,
|
||||
verseDiscoveryPath,
|
||||
replicaBridgePath,
|
||||
mainMenuPath,
|
||||
commandBridgePath,
|
||||
drainStatusPath,
|
||||
|
|
@ -4274,6 +4305,7 @@ static void RequireVerseSettingsShellAndBridge(string root)
|
|||
var verseHostCommands = File.ReadAllText(verseHostCommandsPath);
|
||||
var clientTargetStore = File.ReadAllText(clientTargetStorePath);
|
||||
var verseDiscovery = File.ReadAllText(verseDiscoveryPath);
|
||||
var replicaBridge = File.ReadAllText(replicaBridgePath);
|
||||
var mainMenu = File.ReadAllText(mainMenuPath);
|
||||
var commandBridge = File.ReadAllText(commandBridgePath);
|
||||
var drainStatus = File.ReadAllText(drainStatusPath);
|
||||
|
|
@ -4285,7 +4317,8 @@ static void RequireVerseSettingsShellAndBridge(string root)
|
|||
{
|
||||
"AetheriaRuntimeClientTargetCommands.cs",
|
||||
"AetheriaRuntimeClientTargetSurfaceBuilder.cs",
|
||||
"AetheriaRuntimeVerseDiscovery.cs"
|
||||
"AetheriaRuntimeVerseDiscovery.cs",
|
||||
"AetheriaRuntimeVerseReplicaBridge.cs"
|
||||
};
|
||||
var missingUnityFacadeSymbols = requiredUnityFacadeSymbols
|
||||
.Where(symbol => !unityFacadeProject.Contains(symbol, StringComparison.Ordinal))
|
||||
|
|
@ -4314,6 +4347,7 @@ static void RequireVerseSettingsShellAndBridge(string root)
|
|||
"SetDiscoveryEndpoints",
|
||||
"DiscoverVerses",
|
||||
"SelectDiscoveredVerse",
|
||||
"SyncReplica",
|
||||
"IsKnown"
|
||||
};
|
||||
var missingClientTargetCommandSymbols = requiredClientTargetCommandSymbols
|
||||
|
|
@ -4351,10 +4385,12 @@ static void RequireVerseSettingsShellAndBridge(string root)
|
|||
"\"Verse Discovery\"",
|
||||
"\"Daemon Verse Host\"",
|
||||
"\"Replica State File\"",
|
||||
"\"Replica Sync\"",
|
||||
"AetheriaRuntimeClientTargetCommands.SetStateFilePath",
|
||||
"AetheriaRuntimeClientTargetCommands.SetDiscoveryEndpoints",
|
||||
"AetheriaRuntimeClientTargetCommands.DiscoverVerses",
|
||||
"AetheriaRuntimeClientTargetCommands.SelectDiscoveredVerse",
|
||||
"AetheriaRuntimeClientTargetCommands.SyncReplica",
|
||||
"AetheriaRuntimeVerseHostCommands.CycleVisibility",
|
||||
"AetheriaRuntimeVerseHostCommands.Refresh"
|
||||
};
|
||||
|
|
@ -4390,6 +4426,23 @@ static void RequireVerseSettingsShellAndBridge(string root)
|
|||
string.Join(", ", missingVerseDiscoverySymbols));
|
||||
}
|
||||
|
||||
var requiredReplicaBridgeSymbols = new[]
|
||||
{
|
||||
"public static class AetheriaRuntimeVerseReplicaBridge",
|
||||
"ProcessStartInfo",
|
||||
"Aetheria.State.Replica",
|
||||
"Sync("
|
||||
};
|
||||
var missingReplicaBridgeSymbols = requiredReplicaBridgeSymbols
|
||||
.Where(symbol => !replicaBridge.Contains(symbol, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (missingReplicaBridgeSymbols.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Verse replica bridge no longer exposes the sync transport used by the Unity shell: " +
|
||||
string.Join(", ", missingReplicaBridgeSymbols));
|
||||
}
|
||||
|
||||
var requiredMainMenuSymbols = new[]
|
||||
{
|
||||
"VerseSettingsShellSurfaceId",
|
||||
|
|
@ -4402,6 +4455,7 @@ static void RequireVerseSettingsShellAndBridge(string root)
|
|||
"TryQueueVerseHostCommand(",
|
||||
"AetheriaRuntimeClientTargetStore.Update(",
|
||||
"AetheriaRuntimeVerseDiscovery.RefreshClientTarget(",
|
||||
"AetheriaRuntimeVerseReplicaBridge.Sync(",
|
||||
"ParseDiscoveryEndpoints(",
|
||||
"new EveSurfaceCommandRequest(",
|
||||
"AetheriaRuntimeVerseHostCommands.SurfaceId"
|
||||
|
|
|
|||
|
|
@ -423,6 +423,10 @@ truth. The selected remote client target resolves to that replica path. The
|
|||
daemon still owns accepted state; the replica is only the local read surface
|
||||
Unity lowers from.
|
||||
|
||||
The Unity Verse-settings shell can also trigger the same one-shot sync through
|
||||
the `Aetheria.State.Replica` bridge when the repo-local tool is available. That
|
||||
keeps target selection local while the replica organ owns daemon transport.
|
||||
|
||||
`Aetheria.State.ApplyPending` is the smaller local operator applicator for
|
||||
queued Unity runtime commits and Eve commands when the server host is not being
|
||||
used. It also republishes the typed drain status, operations surface,
|
||||
|
|
|
|||
|
|
@ -458,6 +458,8 @@ public class MainMenu : MonoBehaviour
|
|||
stateBoot.DiscoveredVerses ?? Array.Empty<AetheriaRuntimeDiscoveredVerse>(),
|
||||
stateBoot.LastDiscoveryAtUtc,
|
||||
stateBoot.LastDiscoveryError,
|
||||
stateBoot.LastReplicaSyncAtUtc,
|
||||
stateBoot.LastReplicaSyncError,
|
||||
stateBoot.TargetSource,
|
||||
stateBoot.SupportsLocalStateFileRead,
|
||||
stateBoot.FailureMessage,
|
||||
|
|
@ -626,6 +628,41 @@ public class MainMenu : MonoBehaviour
|
|||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(command, AetheriaRuntimeClientTargetCommands.SyncReplica, StringComparison.Ordinal))
|
||||
{
|
||||
var target = AetheriaRuntimeClientTargetStore.ReadOrInitialize(clientTargetPath, defaultStateFilePath);
|
||||
var syncedAtUtc = DateTime.UtcNow.ToString("O");
|
||||
try
|
||||
{
|
||||
var result = AetheriaRuntimeVerseReplicaBridge.Sync(gameDataDirectory, target);
|
||||
AetheriaRuntimeClientTargetStore.Update(
|
||||
clientTargetPath,
|
||||
defaultStateFilePath,
|
||||
document =>
|
||||
{
|
||||
document.ReplicaStateFilePath = result.ReplicaStateFilePath;
|
||||
document.LastReplicaSyncAtUtc = syncedAtUtc;
|
||||
document.LastReplicaSyncError = "";
|
||||
document.UpdatedAtUtc = syncedAtUtc;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AetheriaRuntimeClientTargetStore.Update(
|
||||
clientTargetPath,
|
||||
defaultStateFilePath,
|
||||
document =>
|
||||
{
|
||||
document.LastReplicaSyncAtUtc = syncedAtUtc;
|
||||
document.LastReplicaSyncError = ex.Message ?? ex.GetType().Name;
|
||||
document.UpdatedAtUtc = syncedAtUtc;
|
||||
});
|
||||
throw;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
AetheriaRuntimeClientTargetStore.Update(
|
||||
clientTargetPath,
|
||||
defaultStateFilePath,
|
||||
|
|
@ -637,15 +674,19 @@ public class MainMenu : MonoBehaviour
|
|||
document.TargetKind = string.Equals(document.TargetKind, AetheriaRuntimeClientTargetKinds.CultMeshVerse, StringComparison.Ordinal)
|
||||
? AetheriaRuntimeClientTargetKinds.StateFile
|
||||
: AetheriaRuntimeClientTargetKinds.CultMeshVerse;
|
||||
document.LastReplicaSyncError = "";
|
||||
break;
|
||||
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetTitle, StringComparison.Ordinal):
|
||||
document.Title = ReadPayloadValue(payload, "value");
|
||||
break;
|
||||
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetVerseId, StringComparison.Ordinal):
|
||||
document.VerseId = ReadPayloadValue(payload, "value");
|
||||
document.ReplicaStateFilePath = AetheriaRuntimeStateBoundary.GetReplicaStateFilePath(gameDataDirectory, document.VerseId);
|
||||
document.LastReplicaSyncError = "";
|
||||
break;
|
||||
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetCultMeshAddress, StringComparison.Ordinal):
|
||||
document.CultMeshAddress = ReadPayloadValue(payload, "value");
|
||||
document.LastReplicaSyncError = "";
|
||||
break;
|
||||
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetStateFilePath, StringComparison.Ordinal):
|
||||
document.StateFilePath = ReadPayloadValue(payload, "value");
|
||||
|
|
@ -659,8 +700,10 @@ public class MainMenu : MonoBehaviour
|
|||
document.Title = ReadPayloadValue(payload, "title");
|
||||
document.VerseId = ReadPayloadValue(payload, "verseId");
|
||||
document.CultMeshAddress = ReadPayloadValue(payload, "cultMeshAddress");
|
||||
document.ReplicaStateFilePath = AetheriaRuntimeStateBoundary.GetReplicaStateFilePath(gameDataDirectory, document.VerseId);
|
||||
document.DiscoveryEndpoints = ParseDiscoveryEndpoints(ReadPayloadValue(payload, "discoveryEndpoints"));
|
||||
document.LastDiscoveryError = "";
|
||||
document.LastReplicaSyncError = "";
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace GameCult.Aetheria.State.Unity
|
|||
public const string SetDiscoveryEndpoints = "aetheria.client_target.discovery_endpoints.set";
|
||||
public const string DiscoverVerses = "aetheria.client_target.discovery.refresh";
|
||||
public const string SelectDiscoveredVerse = "aetheria.client_target.discovery.select";
|
||||
public const string SyncReplica = "aetheria.client_target.replica.sync";
|
||||
|
||||
public static bool IsKnown(string command)
|
||||
{
|
||||
|
|
@ -23,7 +24,8 @@ namespace GameCult.Aetheria.State.Unity
|
|||
command == SetStateFilePath ||
|
||||
command == SetDiscoveryEndpoints ||
|
||||
command == DiscoverVerses ||
|
||||
command == SelectDiscoveredVerse;
|
||||
command == SelectDiscoveredVerse ||
|
||||
command == SyncReplica;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ namespace GameCult.Aetheria.State.Unity
|
|||
[Key(9)] public string LastDiscoveryAtUtc { get; set; } = "";
|
||||
[Key(10)] public string LastDiscoveryError { get; set; } = "";
|
||||
[Key(11)] public string ReplicaStateFilePath { get; set; } = "";
|
||||
[Key(12)] public string LastReplicaSyncAtUtc { get; set; } = "";
|
||||
[Key(13)] public string LastReplicaSyncError { get; set; } = "";
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
@ -96,6 +98,8 @@ namespace GameCult.Aetheria.State.Unity
|
|||
document.LastDiscoveryAtUtc ??= "";
|
||||
document.LastDiscoveryError ??= "";
|
||||
document.ReplicaStateFilePath = NormalizeReplicaStateFilePath(document, clientTargetPath, document.ReplicaStateFilePath);
|
||||
document.LastReplicaSyncAtUtc ??= "";
|
||||
document.LastReplicaSyncError ??= "";
|
||||
foreach (var verse in document.DiscoveredVerses)
|
||||
{
|
||||
if (verse == null)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ namespace GameCult.Aetheria.State.Unity
|
|||
IReadOnlyList<AetheriaRuntimeDiscoveredVerse> discoveredVerses,
|
||||
string lastDiscoveryAtUtc,
|
||||
string lastDiscoveryError,
|
||||
string lastReplicaSyncAtUtc,
|
||||
string lastReplicaSyncError,
|
||||
string targetSource,
|
||||
bool supportsLocalStateFileRead,
|
||||
string bootFailureMessage,
|
||||
|
|
@ -36,6 +38,8 @@ namespace GameCult.Aetheria.State.Unity
|
|||
DiscoveredVerses = discoveredVerses ?? Array.Empty<AetheriaRuntimeDiscoveredVerse>();
|
||||
LastDiscoveryAtUtc = lastDiscoveryAtUtc ?? "";
|
||||
LastDiscoveryError = lastDiscoveryError ?? "";
|
||||
LastReplicaSyncAtUtc = lastReplicaSyncAtUtc ?? "";
|
||||
LastReplicaSyncError = lastReplicaSyncError ?? "";
|
||||
TargetSource = targetSource ?? "";
|
||||
SupportsLocalStateFileRead = supportsLocalStateFileRead;
|
||||
BootFailureMessage = bootFailureMessage ?? "";
|
||||
|
|
@ -56,6 +60,8 @@ namespace GameCult.Aetheria.State.Unity
|
|||
public IReadOnlyList<AetheriaRuntimeDiscoveredVerse> DiscoveredVerses { get; }
|
||||
public string LastDiscoveryAtUtc { get; }
|
||||
public string LastDiscoveryError { get; }
|
||||
public string LastReplicaSyncAtUtc { get; }
|
||||
public string LastReplicaSyncError { get; }
|
||||
public string TargetSource { get; }
|
||||
public bool SupportsLocalStateFileRead { get; }
|
||||
public string BootFailureMessage { get; }
|
||||
|
|
@ -98,6 +104,8 @@ namespace GameCult.Aetheria.State.Unity
|
|||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
supportsLocalStateFileRead: true,
|
||||
bootFailureMessage: "",
|
||||
hostTitle: "",
|
||||
|
|
@ -114,11 +122,12 @@ namespace GameCult.Aetheria.State.Unity
|
|||
? "Make Private"
|
||||
: "Make Public";
|
||||
var lastDiscoveryLabel = string.IsNullOrWhiteSpace(state.LastDiscoveryAtUtc) ? "never" : state.LastDiscoveryAtUtc;
|
||||
var lastReplicaSyncLabel = string.IsNullOrWhiteSpace(state.LastReplicaSyncAtUtc) ? "never" : state.LastReplicaSyncAtUtc;
|
||||
var summaryNote = !state.SupportsLocalStateFileRead && !string.IsNullOrWhiteSpace(state.BootFailureMessage)
|
||||
? state.BootFailureMessage
|
||||
: string.Equals(state.TargetSource, "state-path-override", StringComparison.Ordinal)
|
||||
? "AETHERIA_STATE_PATH is overriding the persisted client target. Update the environment if you want boot to follow the saved target again."
|
||||
: "Client target edits persist in aetheria-client.cc. Verse discovery and selection mutate the same typed owner. Verse visibility changes queue provider-owned Eve commands for the daemon bridge.";
|
||||
: "Client target edits persist in aetheria-client.cc. Verse discovery and selection mutate the same typed owner. Verse visibility changes queue provider-owned Eve commands for the daemon bridge. Remote Verse targets hydrate a cache-only local replica before Unity reads them.";
|
||||
|
||||
var discoveryChildren = new List<AetheriaRuntimeSurfaceComponent>
|
||||
{
|
||||
|
|
@ -217,6 +226,13 @@ namespace GameCult.Aetheria.State.Unity
|
|||
"aetheria.clientTarget.target.replicaStateFilePath",
|
||||
"Replica State File",
|
||||
state.TargetReplicaStateFilePath),
|
||||
Metric(
|
||||
"aetheria.clientTarget.target.replicaSyncAt",
|
||||
"Replica Sync",
|
||||
lastReplicaSyncLabel),
|
||||
Text(
|
||||
"aetheria.clientTarget.target.replicaSyncError",
|
||||
state.LastReplicaSyncError),
|
||||
ButtonRow(
|
||||
"aetheria.clientTarget.target.actions",
|
||||
Button(
|
||||
|
|
@ -225,6 +241,10 @@ namespace GameCult.Aetheria.State.Unity
|
|||
? "Use Local State File"
|
||||
: "Use CultMesh Verse",
|
||||
AetheriaRuntimeClientTargetCommands.CycleTargetKind),
|
||||
Button(
|
||||
"aetheria.clientTarget.target.syncReplica",
|
||||
"Sync Replica",
|
||||
AetheriaRuntimeClientTargetCommands.SyncReplica),
|
||||
Button(
|
||||
"aetheria.clientTarget.target.refresh",
|
||||
"Refresh",
|
||||
|
|
@ -297,6 +317,10 @@ namespace GameCult.Aetheria.State.Unity
|
|||
AetheriaRuntimeClientTargetCommands.SelectDiscoveredVerse,
|
||||
"Select Discovered Verse",
|
||||
"unity-uitoolkit"),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeClientTargetCommands.SyncReplica,
|
||||
"Sync Replica",
|
||||
"unity-uitoolkit"),
|
||||
new AetheriaRuntimeSurfaceCommandTemplate(
|
||||
AetheriaRuntimeVerseHostCommands.CycleVisibility,
|
||||
"Toggle Visibility",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ namespace GameCult.Aetheria.State.Unity
|
|||
string[] discoveryEndpoints,
|
||||
AetheriaRuntimeDiscoveredVerse[] discoveredVerses,
|
||||
string lastDiscoveryAtUtc,
|
||||
string lastDiscoveryError)
|
||||
string lastDiscoveryError,
|
||||
string lastReplicaSyncAtUtc,
|
||||
string lastReplicaSyncError)
|
||||
{
|
||||
ClientTargetPath = clientTargetPath;
|
||||
TargetKind = targetKind;
|
||||
|
|
@ -38,6 +40,8 @@ namespace GameCult.Aetheria.State.Unity
|
|||
DiscoveredVerses = discoveredVerses ?? Array.Empty<AetheriaRuntimeDiscoveredVerse>();
|
||||
LastDiscoveryAtUtc = lastDiscoveryAtUtc ?? "";
|
||||
LastDiscoveryError = lastDiscoveryError ?? "";
|
||||
LastReplicaSyncAtUtc = lastReplicaSyncAtUtc ?? "";
|
||||
LastReplicaSyncError = lastReplicaSyncError ?? "";
|
||||
}
|
||||
|
||||
public string ClientTargetPath { get; }
|
||||
|
|
@ -55,6 +59,8 @@ namespace GameCult.Aetheria.State.Unity
|
|||
public AetheriaRuntimeDiscoveredVerse[] DiscoveredVerses { get; }
|
||||
public string LastDiscoveryAtUtc { get; }
|
||||
public string LastDiscoveryError { get; }
|
||||
public string LastReplicaSyncAtUtc { get; }
|
||||
public string LastReplicaSyncError { get; }
|
||||
|
||||
public string TargetLabel =>
|
||||
string.IsNullOrWhiteSpace(Title)
|
||||
|
|
@ -134,7 +140,9 @@ namespace GameCult.Aetheria.State.Unity
|
|||
target.DiscoveryEndpoints?.Where(value => !string.IsNullOrWhiteSpace(value)).ToArray() ?? Array.Empty<string>(),
|
||||
target.DiscoveredVerses ?? Array.Empty<AetheriaRuntimeDiscoveredVerse>(),
|
||||
target.LastDiscoveryAtUtc ?? "",
|
||||
target.LastDiscoveryError ?? "");
|
||||
target.LastDiscoveryError ?? "",
|
||||
target.LastReplicaSyncAtUtc ?? "",
|
||||
target.LastReplicaSyncError ?? "");
|
||||
}
|
||||
|
||||
private static string BuildTargetLabel(string title, string verseId)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace GameCult.Aetheria.State.Unity
|
||||
{
|
||||
public readonly struct AetheriaRuntimeVerseReplicaSyncResult
|
||||
{
|
||||
public AetheriaRuntimeVerseReplicaSyncResult(
|
||||
string replicaStateFilePath,
|
||||
string standardOutput,
|
||||
string standardError)
|
||||
{
|
||||
ReplicaStateFilePath = replicaStateFilePath ?? "";
|
||||
StandardOutput = standardOutput ?? "";
|
||||
StandardError = standardError ?? "";
|
||||
}
|
||||
|
||||
public string ReplicaStateFilePath { get; }
|
||||
public string StandardOutput { get; }
|
||||
public string StandardError { get; }
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeVerseReplicaBridge
|
||||
{
|
||||
public static AetheriaRuntimeVerseReplicaSyncResult Sync(
|
||||
DirectoryInfo gameDataDirectory,
|
||||
AetheriaRuntimeClientTargetDocument target,
|
||||
TimeSpan? timeout = null)
|
||||
{
|
||||
if (gameDataDirectory == null) throw new ArgumentNullException(nameof(gameDataDirectory));
|
||||
if (target == null) throw new ArgumentNullException(nameof(target));
|
||||
|
||||
var endpoint = target.CultMeshAddress ?? "";
|
||||
if (string.IsNullOrWhiteSpace(endpoint))
|
||||
throw new InvalidOperationException("Cannot sync a Verse replica without a CultMesh endpoint.");
|
||||
|
||||
var verseId = string.IsNullOrWhiteSpace(target.VerseId) ? "unknown-verse" : target.VerseId;
|
||||
var replicaStateFilePath = string.IsNullOrWhiteSpace(target.ReplicaStateFilePath)
|
||||
? AetheriaRuntimeStateBoundary.GetReplicaStateFilePath(gameDataDirectory, verseId)
|
||||
: Path.GetFullPath(target.ReplicaStateFilePath);
|
||||
var repoRoot = gameDataDirectory.Parent?.FullName;
|
||||
if (string.IsNullOrWhiteSpace(repoRoot))
|
||||
throw new InvalidOperationException("Cannot resolve the Aetheria repo root from the GameData directory.");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(replicaStateFilePath) ?? gameDataDirectory.FullName);
|
||||
|
||||
var startInfo = BuildStartInfo(repoRoot, endpoint, replicaStateFilePath, verseId);
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
var output = new StringBuilder();
|
||||
var error = new StringBuilder();
|
||||
|
||||
process.OutputDataReceived += (_, eventArgs) =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(eventArgs.Data))
|
||||
output.AppendLine(eventArgs.Data);
|
||||
};
|
||||
process.ErrorDataReceived += (_, eventArgs) =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(eventArgs.Data))
|
||||
error.AppendLine(eventArgs.Data);
|
||||
};
|
||||
|
||||
if (!process.Start())
|
||||
throw new InvalidOperationException("Failed to start the Aetheria Verse replica sync process.");
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
var waitTimeout = timeout ?? TimeSpan.FromSeconds(90);
|
||||
if (!process.WaitForExit((int)Math.Ceiling(waitTimeout.TotalMilliseconds)))
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
throw new TimeoutException(
|
||||
$"Timed out waiting {waitTimeout.TotalSeconds:0} seconds for the Aetheria Verse replica sync process.");
|
||||
}
|
||||
|
||||
process.WaitForExit();
|
||||
var outputText = output.ToString().Trim();
|
||||
var errorText = error.ToString().Trim();
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
string.IsNullOrWhiteSpace(errorText)
|
||||
? (string.IsNullOrWhiteSpace(outputText)
|
||||
? $"Aetheria Verse replica sync exited with code {process.ExitCode}."
|
||||
: outputText)
|
||||
: errorText);
|
||||
}
|
||||
|
||||
return new AetheriaRuntimeVerseReplicaSyncResult(replicaStateFilePath, outputText, errorText);
|
||||
}
|
||||
|
||||
private static ProcessStartInfo BuildStartInfo(
|
||||
string repoRoot,
|
||||
string endpoint,
|
||||
string replicaStateFilePath,
|
||||
string verseId)
|
||||
{
|
||||
var executablePath = Path.Combine(repoRoot, "Aetheria.State.Replica", "bin", "Debug", "net10.0", "Aetheria.State.Replica.exe");
|
||||
if (File.Exists(executablePath))
|
||||
{
|
||||
return CreateStartInfo(
|
||||
executablePath,
|
||||
BuildArguments("sync", "--endpoint", endpoint, "--replica", replicaStateFilePath, "--verse-id", verseId),
|
||||
repoRoot);
|
||||
}
|
||||
|
||||
var dllPath = Path.Combine(repoRoot, "Aetheria.State.Replica", "bin", "Debug", "net10.0", "Aetheria.State.Replica.dll");
|
||||
if (File.Exists(dllPath))
|
||||
{
|
||||
return CreateStartInfo(
|
||||
"dotnet",
|
||||
BuildArguments(dllPath, "sync", "--endpoint", endpoint, "--replica", replicaStateFilePath, "--verse-id", verseId),
|
||||
repoRoot);
|
||||
}
|
||||
|
||||
var projectPath = Path.Combine(repoRoot, "Aetheria.State.Replica", "Aetheria.State.Replica.csproj");
|
||||
if (!File.Exists(projectPath))
|
||||
throw new FileNotFoundException("Cannot find the Aetheria.State.Replica project.", projectPath);
|
||||
|
||||
return CreateStartInfo(
|
||||
"dotnet",
|
||||
BuildArguments("run", "--project", projectPath, "--", "sync", "--endpoint", endpoint, "--replica", replicaStateFilePath, "--verse-id", verseId),
|
||||
repoRoot);
|
||||
}
|
||||
|
||||
private static ProcessStartInfo CreateStartInfo(string fileName, string arguments, string workingDirectory)
|
||||
{
|
||||
return new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = workingDirectory,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildArguments(params string[] values)
|
||||
{
|
||||
return string.Join(" ", values.Select(QuoteArgument));
|
||||
}
|
||||
|
||||
private static string QuoteArgument(string value)
|
||||
{
|
||||
var text = value ?? "";
|
||||
if (text.Length == 0)
|
||||
return "\"\"";
|
||||
|
||||
if (text.IndexOfAny(new[] { ' ', '\t', '\n', '\r', '"' }) < 0)
|
||||
return text;
|
||||
|
||||
return "\"" + text.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue