976 lines
39 KiB
C#
976 lines
39 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using GameCult.Aetheria.State.Unity;
|
|
using GameCult.Eve.Surface;
|
|
using GameCult.Eve.UnityUIToolkit;
|
|
using TMPro;
|
|
using UniRx;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.UIElements;
|
|
using static Unity.Mathematics.math;
|
|
using float2 = Unity.Mathematics.float2;
|
|
using Random = UnityEngine.Random;
|
|
|
|
public class MainMenu : MonoBehaviour
|
|
{
|
|
private const string MenuSurfaceType = "surface-state";
|
|
private const string MenuSurfaceSchema = "gamecult.eve.surface.v1";
|
|
private const string MenuSurfaceProviderId = "aetheria";
|
|
private const string MenuSurfaceProviderKind = "game.menu";
|
|
private const string MainSurfaceId = "aetheria.main_menu.root";
|
|
private const string SettingsSurfaceId = "aetheria.main_menu.settings";
|
|
private const string InputSettingsSurfaceId = "aetheria.main_menu.input_settings";
|
|
private const string PlayerSettingsShellSurfaceId = "aetheria.main_menu.player_settings";
|
|
private const string VerseSettingsShellSurfaceId = "aetheria.main_menu.verse_settings";
|
|
private const string ContinueRunCommand = "aetheria.main_menu.root.continue";
|
|
private const string NewGameCommand = "aetheria.main_menu.root.new_game";
|
|
private const string ShowSettingsCommand = "aetheria.main_menu.root.show_settings";
|
|
private const string QuitCommand = "aetheria.main_menu.root.quit";
|
|
private const string OpenRuntimeInputScreenCommand = "aetheria.main_menu.input_settings.open_runtime_screen";
|
|
private const string ShowPlayerSettingsCommand = "aetheria.main_menu.settings.show_player_settings";
|
|
private const string ShowVerseSettingsCommand = "aetheria.main_menu.settings.show_verse_settings";
|
|
private const string ShowInputSettingsCommand = "aetheria.main_menu.settings.show_input_settings";
|
|
private const string BackToMainCommand = "aetheria.main_menu.settings.back_to_main";
|
|
private const string BackToSettingsCommand = "aetheria.main_menu.settings.back_to_settings";
|
|
|
|
public VolumeCloudRenderer CloudRenderer;
|
|
public GameSettings Settings;
|
|
public ConfirmationDialog Dialog;
|
|
public bool InGame;
|
|
|
|
private UIDocument _menuSurfaceDocument;
|
|
|
|
void Start()
|
|
{
|
|
ShowMain();
|
|
}
|
|
|
|
private void ShowMain()
|
|
{
|
|
var stateBoot = CurrentStateBoot();
|
|
RenderMenuSurface(
|
|
BuildMainSurfaceDefinition(
|
|
stateBoot,
|
|
LatestContinueRun(stateBoot),
|
|
LatestVerseHostSettings(stateBoot),
|
|
InGame),
|
|
HandleMainSurfaceCommand);
|
|
}
|
|
|
|
private void HandleMainSurfaceCommand(EveSurfaceCommandRequest request)
|
|
{
|
|
switch (request.Command)
|
|
{
|
|
case ContinueRunCommand:
|
|
var continueRun = LatestContinueRun(CurrentStateBoot());
|
|
if (continueRun == null)
|
|
{
|
|
Debug.LogWarning("Main-menu Continue requested without a typed run state.");
|
|
ShowMain();
|
|
return;
|
|
}
|
|
|
|
ContinueGame(continueRun);
|
|
return;
|
|
case NewGameCommand:
|
|
StartNewGame();
|
|
return;
|
|
case ShowSettingsCommand:
|
|
ShowSettings();
|
|
return;
|
|
case QuitCommand:
|
|
Application.Quit();
|
|
return;
|
|
default:
|
|
Debug.LogWarning($"Unknown main menu command: {request.Command}");
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void StartNewGame()
|
|
{
|
|
var generatorState = "Loading typed catalog";
|
|
Action<string> setState = s => generatorState = s;
|
|
|
|
HideMenuSurface();
|
|
Dialog.Clear();
|
|
Dialog.Title.text = "Generating Galaxy";
|
|
Dialog.AddProperty(() => generatorState);
|
|
Dialog.Show();
|
|
|
|
if (ActionGameManager.RuntimePlayerSettings.TutorialPassed)
|
|
{
|
|
Settings.SectorBackgroundSettings.NoisePosition = Random.value * 1000;
|
|
ActionGameManager.IsTutorial = false;
|
|
var generationSeed = NextGenerationSeed();
|
|
Task.Run(() =>
|
|
{
|
|
var sector = new Galaxy(
|
|
Settings.SectorGenerationSettings,
|
|
Settings.SectorBackgroundSettings,
|
|
Settings.NameGeneratorSettings,
|
|
ActionGameManager.RuntimeCatalog,
|
|
Debug.Log,
|
|
setState,
|
|
generationSeed);
|
|
Observable.NextFrame().Subscribe(_ =>
|
|
{
|
|
ActionGameManager.CurrentGalaxy = sector;
|
|
SceneManager.LoadScene("ARPG");
|
|
});
|
|
}).WrapErrors();
|
|
}
|
|
else
|
|
{
|
|
int iteration = 1;
|
|
do
|
|
{
|
|
Settings.TutorialBackgroundSettings.NoisePosition = Random.value * 1000;
|
|
setState($"Finding Galaxy Position: iteration {iteration++}");
|
|
} while (Settings.TutorialBackgroundSettings.CloudDensity(float2(0.5f)) < .5f);
|
|
|
|
ActionGameManager.IsTutorial = true;
|
|
var generationSeed = NextGenerationSeed();
|
|
Task.Run(() =>
|
|
{
|
|
var sector = new Galaxy(
|
|
Settings.TutorialGenerationSettings,
|
|
Settings.TutorialBackgroundSettings,
|
|
Settings.NameGeneratorSettings,
|
|
ActionGameManager.RuntimeCatalog,
|
|
ActionGameManager.RuntimePlayerSettings,
|
|
ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"),
|
|
Debug.Log,
|
|
setState,
|
|
generationSeed);
|
|
Observable.NextFrame().Subscribe(_ =>
|
|
{
|
|
ActionGameManager.CurrentGalaxy = sector;
|
|
SceneManager.LoadScene("ARPG");
|
|
});
|
|
}).WrapErrors();
|
|
}
|
|
}
|
|
|
|
private static uint NextGenerationSeed()
|
|
{
|
|
var seed = (uint)Random.Range(1, int.MaxValue);
|
|
return seed == 0 ? 1u : seed;
|
|
}
|
|
|
|
private static AetheriaRuntimeStateBootReport CurrentStateBoot()
|
|
{
|
|
return AetheriaRuntimeStateBoot.Inspect(ActionGameManager.GameDataDirectory);
|
|
}
|
|
|
|
private static AetheriaRuntimeRunStateSnapshot LatestContinueRun(AetheriaRuntimeStateBootReport stateBoot)
|
|
{
|
|
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
return AetheriaRuntimeStateReader
|
|
.ReadRunStates(stateBoot.StateFilePath)
|
|
.Where(run => !string.IsNullOrWhiteSpace(run.RunId))
|
|
.OrderByDescending(run => run.UpdatedAtUtc, StringComparer.Ordinal)
|
|
.ThenByDescending(run => run.RunId, StringComparer.Ordinal)
|
|
.FirstOrDefault();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"Failed to read typed Aetheria run state for Continue: {ex}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static AetheriaRuntimeVerseHostSettingsSnapshot LatestVerseHostSettings(AetheriaRuntimeStateBootReport stateBoot)
|
|
{
|
|
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
return AetheriaRuntimeStateReader.ReadVerseHostSettings(stateBoot.StateFilePath);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"Failed to read typed Aetheria Verse host settings for the main menu: {ex}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void ContinueGame(AetheriaRuntimeRunStateSnapshot run)
|
|
{
|
|
if (run == null)
|
|
return;
|
|
|
|
var generatorState = "Loading typed run";
|
|
Action<string> setState = s => generatorState = s;
|
|
|
|
HideMenuSurface();
|
|
Dialog.Clear();
|
|
Dialog.Title.text = "Continuing Run";
|
|
Dialog.AddProperty(() => generatorState);
|
|
Dialog.Show();
|
|
|
|
ActionGameManager.IsTutorial = run.IsTutorial;
|
|
ActionGameManager.ContinueRunState = run;
|
|
var generationSeed = run.GenerationSeed == 0 ? NextGenerationSeed() : run.GenerationSeed;
|
|
var backgroundSettings = run.IsTutorial ? Settings.TutorialBackgroundSettings : Settings.SectorBackgroundSettings;
|
|
backgroundSettings.NoisePosition = generationSeed;
|
|
|
|
Task.Run(() =>
|
|
{
|
|
var sector = run.IsTutorial
|
|
? new Galaxy(
|
|
Settings.TutorialGenerationSettings,
|
|
Settings.TutorialBackgroundSettings,
|
|
Settings.NameGeneratorSettings,
|
|
ActionGameManager.RuntimeCatalog,
|
|
ActionGameManager.RuntimePlayerSettings,
|
|
ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"),
|
|
Debug.Log,
|
|
setState,
|
|
generationSeed)
|
|
: new Galaxy(
|
|
Settings.SectorGenerationSettings,
|
|
backgroundSettings,
|
|
Settings.NameGeneratorSettings,
|
|
ActionGameManager.RuntimeCatalog,
|
|
Debug.Log,
|
|
setState,
|
|
generationSeed);
|
|
Observable.NextFrame().Subscribe(_ =>
|
|
{
|
|
ActionGameManager.CurrentGalaxy = sector;
|
|
SceneManager.LoadScene("ARPG");
|
|
});
|
|
}).WrapErrors();
|
|
}
|
|
|
|
private void ShowSettings()
|
|
{
|
|
RenderMenuSurface(BuildSettingsSurfaceDefinition(), HandleSettingsSurfaceCommand);
|
|
}
|
|
|
|
private void ShowInputSettings()
|
|
{
|
|
RenderMenuSurface(BuildInputSettingsSurfaceDefinition(CanOpenRuntimeInputScreen(), InGame), HandleInputSettingsSurfaceCommand);
|
|
}
|
|
|
|
private void ShowPlayerSettingsSurface()
|
|
{
|
|
RenderMenuSurface(
|
|
WithBackAction(
|
|
ToEveSurfaceDocument(BuildPlayerSettingsSurfaceDefinition()),
|
|
PlayerSettingsShellSurfaceId,
|
|
BackToSettingsCommand,
|
|
"Back"),
|
|
HandlePlayerSettingsSurfaceCommand);
|
|
}
|
|
|
|
private void ShowVerseSettingsSurface()
|
|
{
|
|
RenderMenuSurface(
|
|
WithBackAction(
|
|
ToEveSurfaceDocument(BuildVerseSettingsSurfaceDefinition()),
|
|
VerseSettingsShellSurfaceId,
|
|
BackToSettingsCommand,
|
|
"Back"),
|
|
HandleVerseSettingsSurfaceCommand);
|
|
}
|
|
|
|
private void RenderMenuSurface(
|
|
EveSurfaceDocument document,
|
|
Action<EveSurfaceCommandRequest> commandHandler)
|
|
{
|
|
var surfaceDocument = ResolveMenuSurfaceDocument();
|
|
surfaceDocument.gameObject.SetActive(true);
|
|
|
|
var root = surfaceDocument.rootVisualElement;
|
|
root.Clear();
|
|
root.style.flexGrow = 1;
|
|
root.style.justifyContent = Justify.Center;
|
|
root.style.alignItems = Align.Center;
|
|
root.style.paddingLeft = 24;
|
|
root.style.paddingRight = 24;
|
|
root.style.paddingTop = 24;
|
|
root.style.paddingBottom = 24;
|
|
root.style.backgroundColor = new Color(0f, 0f, 0f, 0.6f);
|
|
|
|
var shell = new VisualElement();
|
|
shell.style.flexDirection = FlexDirection.Column;
|
|
shell.style.width = 560;
|
|
shell.style.maxWidth = 560;
|
|
shell.style.paddingLeft = 20;
|
|
shell.style.paddingRight = 20;
|
|
shell.style.paddingTop = 20;
|
|
shell.style.paddingBottom = 20;
|
|
shell.style.backgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.96f);
|
|
root.Add(shell);
|
|
|
|
var lowerer = new EveUiToolkitSurfaceLowerer();
|
|
shell.Add(lowerer.Lower(document, commandHandler));
|
|
}
|
|
|
|
private void HandleSettingsSurfaceCommand(EveSurfaceCommandRequest request)
|
|
{
|
|
switch (request.Command)
|
|
{
|
|
case ShowPlayerSettingsCommand:
|
|
ShowPlayerSettingsSurface();
|
|
return;
|
|
case ShowVerseSettingsCommand:
|
|
ShowVerseSettingsSurface();
|
|
return;
|
|
case ShowInputSettingsCommand:
|
|
ShowInputSettings();
|
|
return;
|
|
case BackToMainCommand:
|
|
ShowMain();
|
|
return;
|
|
default:
|
|
Debug.LogWarning($"Unknown settings menu command: {request.Command}");
|
|
return;
|
|
}
|
|
}
|
|
|
|
private void HandleInputSettingsSurfaceCommand(EveSurfaceCommandRequest request)
|
|
{
|
|
if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal))
|
|
{
|
|
ShowSettings();
|
|
return;
|
|
}
|
|
|
|
if (string.Equals(request.Command, OpenRuntimeInputScreenCommand, StringComparison.Ordinal))
|
|
{
|
|
if (!TryOpenRuntimeInputScreen())
|
|
{
|
|
ShowInputSettings();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
Debug.LogWarning($"Unknown input settings command: {request.Command}");
|
|
}
|
|
|
|
private void HandlePlayerSettingsSurfaceCommand(EveSurfaceCommandRequest request)
|
|
{
|
|
if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal))
|
|
{
|
|
ShowSettings();
|
|
return;
|
|
}
|
|
|
|
if (!ActionGameManager.CommitRuntimePlayerSettingsCommand(request.Command, request.Payload))
|
|
{
|
|
Debug.LogWarning($"Unknown player-settings command: {request.Command}");
|
|
return;
|
|
}
|
|
|
|
CloudRenderer.quality = ActionGameManager.RuntimePlayerSettings.GraphicsSettings.NebulaQuality;
|
|
ShowPlayerSettingsSurface();
|
|
}
|
|
|
|
private void HandleVerseSettingsSurfaceCommand(EveSurfaceCommandRequest request)
|
|
{
|
|
if (string.Equals(request.Command, BackToSettingsCommand, StringComparison.Ordinal))
|
|
{
|
|
ShowSettings();
|
|
return;
|
|
}
|
|
|
|
if (TryCommitClientTargetCommand(request.Command, request.Payload))
|
|
{
|
|
ShowVerseSettingsSurface();
|
|
return;
|
|
}
|
|
|
|
if (TryQueueVerseHostCommand(request.Command, request.Payload))
|
|
{
|
|
ShowVerseSettingsSurface();
|
|
return;
|
|
}
|
|
|
|
Debug.LogWarning($"Unknown verse-settings command: {request.Command}");
|
|
}
|
|
|
|
private void HideMenuSurface()
|
|
{
|
|
if (_menuSurfaceDocument == null)
|
|
return;
|
|
|
|
_menuSurfaceDocument.rootVisualElement.Clear();
|
|
_menuSurfaceDocument.gameObject.SetActive(false);
|
|
}
|
|
|
|
private UIDocument ResolveMenuSurfaceDocument()
|
|
{
|
|
if (_menuSurfaceDocument != null)
|
|
return _menuSurfaceDocument;
|
|
|
|
var host = new GameObject("Aetheria Menu Surface");
|
|
host.transform.SetParent(transform, false);
|
|
var document = host.AddComponent<UIDocument>();
|
|
document.sortingOrder = 1000;
|
|
host.SetActive(false);
|
|
_menuSurfaceDocument = document;
|
|
return document;
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceDocument BuildPlayerSettingsSurfaceDefinition()
|
|
{
|
|
return AetheriaRuntimePlayerSettingsSurfaceBuilder.Build(
|
|
new AetheriaRuntimePlayerSettingsSurfaceState(
|
|
ActionGameManager.RuntimePlayerSettings.Name,
|
|
ActionGameManager.RuntimePlayerSettings.TutorialPassed,
|
|
ActionGameManager.ContinueRunState?.RunId ?? "",
|
|
ActionGameManager.RuntimePlayerSettings.GameplaySettings.TemperatureUnit.ToString(),
|
|
max(0, ActionGameManager.RuntimePlayerSettings.GameplaySettings.SignificantDigits),
|
|
ActionGameManager.RuntimePlayerSettings.GraphicsSettings.NebulaQuality.ToString(),
|
|
ActionGameManager.RuntimePlayerSettings.GraphicsSettings.ShowAsteroidsInMinimap,
|
|
DateTime.UtcNow.ToString("O")));
|
|
}
|
|
|
|
private static AetheriaRuntimeSurfaceDocument BuildVerseSettingsSurfaceDefinition()
|
|
{
|
|
var stateBoot = CurrentStateBoot();
|
|
var verseHost = LatestVerseHostSettings(stateBoot);
|
|
return AetheriaRuntimeClientTargetSurfaceBuilder.Build(
|
|
new AetheriaRuntimeClientTargetSurfaceState(
|
|
stateBoot.TargetKind,
|
|
stateBoot.Title,
|
|
stateBoot.VerseId,
|
|
stateBoot.CultMeshAddress,
|
|
stateBoot.StateFilePath,
|
|
stateBoot.ReplicaStateFilePath,
|
|
string.Join(", ", stateBoot.DiscoveryEndpoints ?? Array.Empty<string>()),
|
|
stateBoot.DiscoveredVerses ?? Array.Empty<AetheriaRuntimeDiscoveredVerse>(),
|
|
stateBoot.LastDiscoveryAtUtc,
|
|
stateBoot.LastDiscoveryError,
|
|
stateBoot.LastReplicaSyncAtUtc,
|
|
stateBoot.LastReplicaSyncError,
|
|
stateBoot.TargetSource,
|
|
stateBoot.SupportsLocalStateFileRead,
|
|
stateBoot.FailureMessage,
|
|
verseHost?.Title ?? stateBoot.Title,
|
|
verseHost?.VerseId ?? stateBoot.VerseId,
|
|
verseHost?.Visibility ?? "unknown",
|
|
verseHost?.CultMeshAddress ?? stateBoot.CultMeshAddress,
|
|
DateTime.UtcNow.ToString("O")));
|
|
}
|
|
|
|
private static EveSurfaceDocument BuildMainSurfaceDefinition(
|
|
AetheriaRuntimeStateBootReport stateBoot,
|
|
AetheriaRuntimeRunStateSnapshot continueRun,
|
|
AetheriaRuntimeVerseHostSettingsSnapshot verseHost,
|
|
bool inGame)
|
|
{
|
|
var commands = new List<EveCommandTemplate>();
|
|
var actionButtons = new List<EveSurfaceComponent>();
|
|
var cardChildren = new List<EveSurfaceComponent>();
|
|
var targetLabel = stateBoot.TargetLabel;
|
|
var targetKind = string.IsNullOrWhiteSpace(stateBoot.TargetKind) ? "unknown" : stateBoot.TargetKind;
|
|
var targetSource = string.IsNullOrWhiteSpace(stateBoot.TargetSource) ? "unknown" : stateBoot.TargetSource;
|
|
var verseTitle = string.IsNullOrWhiteSpace(verseHost?.Title) ? "Unknown Verse" : verseHost.Title;
|
|
var verseId = string.IsNullOrWhiteSpace(verseHost?.VerseId) ? "unknown" : verseHost.VerseId;
|
|
var verseVisibility = string.IsNullOrWhiteSpace(verseHost?.Visibility) ? "unknown" : verseHost.Visibility;
|
|
var verseMeshAddress = string.IsNullOrWhiteSpace(verseHost?.CultMeshAddress) ? "unpublished" : verseHost.CultMeshAddress;
|
|
var verseLabel = string.Equals(verseTitle, verseId, StringComparison.Ordinal)
|
|
? verseTitle
|
|
: $"{verseTitle} ({verseId})";
|
|
|
|
if (!inGame)
|
|
{
|
|
if (!stateBoot.SupportsLocalStateFileRead)
|
|
{
|
|
cardChildren.Add(Text(
|
|
$"{MainSurfaceId}.continue.note",
|
|
stateBoot.FailureMessage));
|
|
}
|
|
else if (continueRun == null)
|
|
{
|
|
cardChildren.Add(Text(
|
|
$"{MainSurfaceId}.continue.note",
|
|
$"No typed run state is available in {verseLabel} yet. Start a new galaxy or connect to a Verse that already has one."));
|
|
}
|
|
else
|
|
{
|
|
commands.Add(new EveCommandTemplate(ContinueRunCommand, "Continue", "unity-uitoolkit"));
|
|
actionButtons.Add(Button($"{MainSurfaceId}.continue", "Continue", ContinueRunCommand));
|
|
cardChildren.Add(Metric(
|
|
$"{MainSurfaceId}.continue.run",
|
|
"Latest Run",
|
|
continueRun.RunId));
|
|
}
|
|
}
|
|
|
|
cardChildren.Add(Metric(
|
|
$"{MainSurfaceId}.target.title",
|
|
"Client Target",
|
|
targetLabel));
|
|
cardChildren.Add(Metric(
|
|
$"{MainSurfaceId}.target.transport",
|
|
"Transport",
|
|
targetKind));
|
|
cardChildren.Add(Metric(
|
|
$"{MainSurfaceId}.target.source",
|
|
"Target Source",
|
|
targetSource));
|
|
|
|
cardChildren.Add(Metric(
|
|
$"{MainSurfaceId}.verse.title",
|
|
"Verse",
|
|
verseLabel));
|
|
cardChildren.Add(Metric(
|
|
$"{MainSurfaceId}.verse.visibility",
|
|
"Visibility",
|
|
verseVisibility));
|
|
cardChildren.Add(Metric(
|
|
$"{MainSurfaceId}.verse.mesh",
|
|
"CultMesh",
|
|
verseMeshAddress));
|
|
|
|
commands.Add(new EveCommandTemplate(NewGameCommand, "New Game", "unity-uitoolkit"));
|
|
commands.Add(new EveCommandTemplate(ShowSettingsCommand, "Settings", "unity-uitoolkit"));
|
|
commands.Add(new EveCommandTemplate(QuitCommand, "Quit", "unity-uitoolkit"));
|
|
|
|
actionButtons.Add(Button($"{MainSurfaceId}.newGame", "New Game", NewGameCommand));
|
|
actionButtons.Add(Button($"{MainSurfaceId}.settings", "Settings", ShowSettingsCommand));
|
|
actionButtons.Add(Button($"{MainSurfaceId}.quit", "Quit", QuitCommand));
|
|
|
|
cardChildren.Add(Text(
|
|
$"{MainSurfaceId}.note",
|
|
$"The client lowers this shell through Eve. The client target chooses which Verse it follows; game truth belongs to the daemon serving {verseLabel}."));
|
|
cardChildren.Add(ButtonColumn($"{MainSurfaceId}.actions", actionButtons.ToArray()));
|
|
|
|
return BuildMenuSurfaceDocument(
|
|
MainSurfaceId,
|
|
"Aetheria Terminus",
|
|
commands,
|
|
Card(
|
|
$"{MainSurfaceId}.card",
|
|
"Aetheria Terminus",
|
|
cardChildren.ToArray()));
|
|
}
|
|
|
|
private static EveSurfaceDocument BuildSettingsSurfaceDefinition()
|
|
{
|
|
return BuildMenuSurfaceDocument(
|
|
SettingsSurfaceId,
|
|
"Aetheria Settings",
|
|
new[]
|
|
{
|
|
new EveCommandTemplate(ShowPlayerSettingsCommand, "Player Settings", "unity-uitoolkit"),
|
|
new EveCommandTemplate(ShowVerseSettingsCommand, "Verse", "unity-uitoolkit"),
|
|
new EveCommandTemplate(ShowInputSettingsCommand, "Input", "unity-uitoolkit"),
|
|
new EveCommandTemplate(BackToMainCommand, "Back", "unity-uitoolkit")
|
|
},
|
|
Card(
|
|
"aetheria.mainMenu.settings.card",
|
|
"Settings",
|
|
Text(
|
|
"aetheria.mainMenu.settings.note",
|
|
"Player settings, client target selection, and input rebinding all lower through typed Eve surfaces. Audio still has no typed owner."),
|
|
ButtonRow(
|
|
"aetheria.mainMenu.settings.actions",
|
|
Button("aetheria.mainMenu.settings.playerSettings", "Player Settings", ShowPlayerSettingsCommand),
|
|
Button("aetheria.mainMenu.settings.verse", "Verse", ShowVerseSettingsCommand),
|
|
Button("aetheria.mainMenu.settings.input", "Input", ShowInputSettingsCommand),
|
|
Button("aetheria.mainMenu.settings.back", "Back", BackToMainCommand))));
|
|
}
|
|
|
|
private bool CanOpenRuntimeInputScreen()
|
|
{
|
|
return InGame &&
|
|
ActionGameManager.Instance != null &&
|
|
ActionGameManager.Instance.CanShowInputScreenFromMenu() &&
|
|
ActionGameManager.Instance.InputDisplayLayout != null;
|
|
}
|
|
|
|
private bool TryOpenRuntimeInputScreen()
|
|
{
|
|
if (!CanOpenRuntimeInputScreen())
|
|
return false;
|
|
|
|
HideMenuSurface();
|
|
gameObject.SetActive(false);
|
|
ActionGameManager.Instance.ShowInputScreenFromMenu();
|
|
return true;
|
|
}
|
|
|
|
private static bool TryCommitClientTargetCommand(string command, IReadOnlyDictionary<string, string> payload)
|
|
{
|
|
if (!AetheriaRuntimeClientTargetCommands.IsKnown(command))
|
|
return false;
|
|
|
|
if (string.Equals(command, AetheriaRuntimeClientTargetCommands.Refresh, StringComparison.Ordinal))
|
|
return true;
|
|
|
|
try
|
|
{
|
|
var gameDataDirectory = ActionGameManager.GameDataDirectory;
|
|
var clientTargetPath = AetheriaRuntimeStateBoundary.GetClientTargetPath(gameDataDirectory);
|
|
var defaultStateFilePath = AetheriaRuntimeStateBoundary.GetStateFilePath(gameDataDirectory);
|
|
if (string.Equals(command, AetheriaRuntimeClientTargetCommands.DiscoverVerses, StringComparison.Ordinal))
|
|
{
|
|
AetheriaRuntimeVerseDiscovery.RefreshClientTarget(clientTargetPath, defaultStateFilePath);
|
|
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,
|
|
document =>
|
|
{
|
|
switch (command)
|
|
{
|
|
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.CycleTargetKind, StringComparison.Ordinal):
|
|
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");
|
|
break;
|
|
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SetDiscoveryEndpoints, StringComparison.Ordinal):
|
|
document.DiscoveryEndpoints = ParseDiscoveryEndpoints(ReadPayloadValue(payload, "value"));
|
|
document.LastDiscoveryError = "";
|
|
break;
|
|
case var _ when string.Equals(command, AetheriaRuntimeClientTargetCommands.SelectDiscoveredVerse, StringComparison.Ordinal):
|
|
document.TargetKind = AetheriaRuntimeClientTargetKinds.CultMeshVerse;
|
|
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;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(document.TargetKind))
|
|
document.TargetKind = AetheriaRuntimeClientTargetKinds.StateFile;
|
|
if (string.IsNullOrWhiteSpace(document.StateFilePath))
|
|
document.StateFilePath = defaultStateFilePath;
|
|
document.UpdatedAtUtc = DateTime.UtcNow.ToString("O");
|
|
});
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"Failed to update typed Aetheria client target: {ex}");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static bool TryQueueVerseHostCommand(string command, IReadOnlyDictionary<string, string> payload)
|
|
{
|
|
if (!AetheriaRuntimeVerseHostCommands.IsKnown(command))
|
|
return false;
|
|
|
|
var stateBoot = CurrentStateBoot();
|
|
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
|
|
{
|
|
Debug.LogWarning(
|
|
$"Cannot queue Verse-host command '{command}' because the active target is not a readable local Verse state file.");
|
|
return true;
|
|
}
|
|
|
|
try
|
|
{
|
|
var request = new EveSurfaceCommandRequest(
|
|
"aetheria",
|
|
AetheriaRuntimeVerseHostCommands.SurfaceId,
|
|
command,
|
|
payload?.ToDictionary(entry => entry.Key, entry => entry.Value ?? "", StringComparer.Ordinal)
|
|
?? new Dictionary<string, string>(StringComparer.Ordinal),
|
|
DateTimeOffset.UtcNow,
|
|
"unity-main-menu");
|
|
var envelope = AetheriaRuntimeEveCommandLog.QueueCommand(stateBoot.StateFilePath, request);
|
|
Debug.Log($"Queued Aetheria Verse-host Eve command: {envelope.Path}");
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"Failed to queue typed Aetheria Verse-host Eve command: {ex}");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private static string ReadPayloadValue(IReadOnlyDictionary<string, string> payload, string key)
|
|
{
|
|
return payload != null && payload.TryGetValue(key, out var value)
|
|
? value ?? ""
|
|
: "";
|
|
}
|
|
|
|
private static string[] ParseDiscoveryEndpoints(string value)
|
|
{
|
|
return (value ?? "")
|
|
.Split(new[] { ',', ';', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(endpoint => endpoint.Trim())
|
|
.Where(endpoint => !string.IsNullOrWhiteSpace(endpoint))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToArray();
|
|
}
|
|
|
|
private static EveSurfaceDocument BuildInputSettingsSurfaceDefinition(bool canOpenRuntimeInputScreen, bool inGame)
|
|
{
|
|
var commands = new List<EveCommandTemplate>
|
|
{
|
|
new EveCommandTemplate(BackToSettingsCommand, "Back", "unity-uitoolkit")
|
|
};
|
|
|
|
var cardChildren = new List<EveSurfaceComponent>
|
|
{
|
|
Metric(
|
|
"aetheria.mainMenu.input.bindingOverrides",
|
|
"Binding Overrides",
|
|
ActionGameManager.RuntimePlayerSettings.InputSettings.InputActionMap.Count.ToString()),
|
|
Metric(
|
|
"aetheria.mainMenu.input.actionBarInputs",
|
|
"Action-Bar Inputs",
|
|
ActionGameManager.RuntimePlayerSettings.InputSettings.ActionBarInputs.Count.ToString())
|
|
};
|
|
|
|
if (canOpenRuntimeInputScreen)
|
|
{
|
|
commands.Insert(0, new EveCommandTemplate(OpenRuntimeInputScreenCommand, "Open Remap Screen", "unity-uitoolkit"));
|
|
cardChildren.Add(Text(
|
|
"aetheria.mainMenu.input.note",
|
|
"The runtime Eve input screen owns low-level InputSystem rebinding and action-bar input edits. This title shell reports typed player-settings state and hands off to that owner."));
|
|
}
|
|
else if (inGame)
|
|
{
|
|
cardChildren.Add(Text(
|
|
"aetheria.mainMenu.input.note",
|
|
"The runtime Eve input screen should own rebinding here, but this scene has no active input surface to hand off to."));
|
|
}
|
|
else
|
|
{
|
|
cardChildren.Add(Text(
|
|
"aetheria.mainMenu.input.note",
|
|
"This title shell reports the typed player-settings state. Launch a run to open the runtime Eve input screen that owns low-level InputSystem rebinding."));
|
|
}
|
|
|
|
var buttons = new List<EveSurfaceComponent>();
|
|
if (canOpenRuntimeInputScreen)
|
|
{
|
|
buttons.Add(Button("aetheria.mainMenu.input.openRuntimeScreen", "Open Remap Screen", OpenRuntimeInputScreenCommand));
|
|
}
|
|
|
|
buttons.Add(Button("aetheria.mainMenu.input.back", "Back", BackToSettingsCommand));
|
|
cardChildren.Add(ButtonRow("aetheria.mainMenu.input.actions", buttons.ToArray()));
|
|
|
|
return BuildMenuSurfaceDocument(
|
|
InputSettingsSurfaceId,
|
|
"Aetheria Input Settings",
|
|
commands,
|
|
Card(
|
|
"aetheria.mainMenu.input.card",
|
|
"Input Settings",
|
|
cardChildren.ToArray()));
|
|
}
|
|
|
|
private static EveSurfaceDocument WithBackAction(
|
|
EveSurfaceDocument document,
|
|
string surfaceId,
|
|
string backCommand,
|
|
string backLabel)
|
|
{
|
|
return new EveSurfaceDocument(
|
|
document.Type,
|
|
document.Schema,
|
|
document.ProviderId,
|
|
document.ProviderKind,
|
|
document.Title,
|
|
document.Version,
|
|
document.UpdatedAtUtc,
|
|
new EveSurfaceTree(
|
|
surfaceId,
|
|
Node(
|
|
$"{surfaceId}.root",
|
|
"surface",
|
|
Array.Empty<(string Key, string Value)>(),
|
|
document.Surface.Root,
|
|
ButtonRow(
|
|
$"{surfaceId}.actions",
|
|
Button($"{surfaceId}.back", backLabel, backCommand))),
|
|
document.Surface.Styles),
|
|
document.Commands
|
|
.Concat(new[]
|
|
{
|
|
new EveCommandTemplate(backCommand, backLabel, "unity-uitoolkit")
|
|
})
|
|
.ToArray());
|
|
}
|
|
|
|
private static EveSurfaceDocument BuildMenuSurfaceDocument(
|
|
string surfaceId,
|
|
string title,
|
|
IReadOnlyList<EveCommandTemplate> commands,
|
|
params EveSurfaceComponent[] children)
|
|
{
|
|
return new EveSurfaceDocument(
|
|
MenuSurfaceType,
|
|
MenuSurfaceSchema,
|
|
MenuSurfaceProviderId,
|
|
MenuSurfaceProviderKind,
|
|
title,
|
|
version: 1,
|
|
DateTime.UtcNow.ToString("O"),
|
|
new EveSurfaceTree(
|
|
surfaceId,
|
|
Node($"{surfaceId}.root", "surface", Array.Empty<(string Key, string Value)>(), children),
|
|
Array.Empty<EveStyleToken>()),
|
|
commands);
|
|
}
|
|
|
|
private static EveSurfaceComponent Card(
|
|
string id,
|
|
string title,
|
|
params EveSurfaceComponent[] children)
|
|
{
|
|
return Node(id, "card", new[] { ("title", title) }, children);
|
|
}
|
|
|
|
private static EveSurfaceComponent Metric(string id, string label, string value)
|
|
{
|
|
return Node(id, "metric", new[] { ("label", label), ("value", value) });
|
|
}
|
|
|
|
private static EveSurfaceComponent Text(string id, string value)
|
|
{
|
|
return Node(id, "text", new[] { ("value", value) });
|
|
}
|
|
|
|
private static EveSurfaceComponent Button(string id, string label, string command)
|
|
{
|
|
return Node(id, "control.button", new[] { ("label", label), ("command", command) });
|
|
}
|
|
|
|
private static EveSurfaceComponent ButtonRow(
|
|
string id,
|
|
params EveSurfaceComponent[] children)
|
|
{
|
|
return Node(id, "row", Array.Empty<(string Key, string Value)>(), children);
|
|
}
|
|
|
|
private static EveSurfaceComponent ButtonColumn(
|
|
string id,
|
|
params EveSurfaceComponent[] children)
|
|
{
|
|
return Node(id, "column", Array.Empty<(string Key, string Value)>(), children);
|
|
}
|
|
|
|
private static EveSurfaceComponent Node(
|
|
string id,
|
|
string kind,
|
|
IEnumerable<(string Key, string Value)> props,
|
|
params EveSurfaceComponent[] children)
|
|
{
|
|
return new EveSurfaceComponent(
|
|
id,
|
|
kind,
|
|
props.ToDictionary(prop => prop.Key, prop => prop.Value, StringComparer.Ordinal),
|
|
children ?? Array.Empty<EveSurfaceComponent>());
|
|
}
|
|
|
|
private static EveSurfaceDocument ToEveSurfaceDocument(AetheriaRuntimeSurfaceDocument document)
|
|
{
|
|
return new EveSurfaceDocument(
|
|
"surface-state",
|
|
"gamecult.eve.surface.v1",
|
|
document.ProviderId,
|
|
document.ProviderKind,
|
|
document.Title,
|
|
document.Version,
|
|
document.UpdatedAtUtc,
|
|
new EveSurfaceTree(
|
|
document.Surface.Id,
|
|
ToEveSurfaceComponent(document.Surface.Root),
|
|
document.Surface.Styles
|
|
.Select(style => new EveStyleToken(style.Name, style.Value))
|
|
.ToArray()),
|
|
document.Commands
|
|
.Select(command => new EveCommandTemplate(command.Command, command.Label, command.Transport))
|
|
.ToArray());
|
|
}
|
|
|
|
private static EveSurfaceComponent ToEveSurfaceComponent(AetheriaRuntimeSurfaceComponent component)
|
|
{
|
|
return new EveSurfaceComponent(
|
|
component.Id,
|
|
component.Kind,
|
|
new Dictionary<string, string>(component.Props, StringComparer.Ordinal),
|
|
component.Children.Select(ToEveSurfaceComponent).ToArray());
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (_menuSurfaceDocument != null)
|
|
{
|
|
Destroy(_menuSurfaceDocument.gameObject);
|
|
_menuSurfaceDocument = null;
|
|
}
|
|
}
|
|
}
|