Aetheria/Assets/Scripts/UI/MainMenu.cs
Meta Vens 6709b06e2e Ship preload: a mod ship that fails to load or assemble is skipped, not fatal
PreloadAsync rethrew on the first failing hull, so one bad package faulted
Loading and no mod ship could spawn. Each hull now loads in its own try: a
failure destroys its partial visual, is logged with the hull's name, and the
rest still preload, as Compose skips a bad package. The play smoke installs the
package beside a broken sibling that composes but cannot assemble (its map icon
moved onto a meshless muzzle node) and proves the sibling has no prototype while
the good ship spawns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:38:35 +02:00

325 lines
12 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 MessagePack;
using TMPro;
using UniRx;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using static CultMath.math;
using float2 = CultMath.float2;
using Random = UnityEngine.Random;
public class MainMenu : MonoBehaviour
{
public VolumeCloudRenderer CloudRenderer;
public GameSettings Settings;
public ConfirmationDialog Dialog;
public bool InGame;
public Prototype PanelPrototype;
public float FadeTime = .5f;
public float FadeDistance = 512;
public float FadeAlphaExponent = 2;
public float FadePositionExponent = 2;
private (PropertiesPanel panel, CanvasGroup group) _currentMenu, _nextMenu;
private bool _fadeFromRight;
private float _fadeLerp;
private bool _fading;
private Vector3 _panelPosition;
//private Task<DatabaseCache> _databaseLoad;
void Start()
{
// Start loading the database in the background 'cause it takes a few seconds
//_databaseLoad = Task.Run(() => ActionGameManager.Database);
_panelPosition = PanelPrototype.transform.position;
var panel1 = PanelPrototype.Instantiate<PropertiesPanel>();
_currentMenu = (panel1, panel1.GetComponent<CanvasGroup>());
var panel2 = PanelPrototype.Instantiate<PropertiesPanel>();
_nextMenu = (panel2, panel2.GetComponent<CanvasGroup>());
_currentMenu.panel.gameObject.SetActive(false);
//_saveDirectory = ActionGameManager.GameDataDirectory.CreateSubdirectory("Saves");
ShowMain();
Fade(true);
}
private void Update()
{
if (_fading)
{
_fadeLerp += Time.deltaTime / FadeTime;
_currentMenu.panel.transform.position =
_panelPosition + (_fadeFromRight ? Vector3.left : Vector3.right) * (FadeDistance * pow(_fadeLerp, FadePositionExponent));
_nextMenu.panel.transform.position =
_panelPosition + (_fadeFromRight ? Vector3.right : Vector3.left) * (FadeDistance * pow(1-_fadeLerp, FadePositionExponent));
_currentMenu.group.alpha = pow(1 - _fadeLerp, FadeAlphaExponent);
_nextMenu.group.alpha = pow(_fadeLerp, FadeAlphaExponent);
if (_fadeLerp > 1)
{
_fading = false;
_currentMenu.panel.gameObject.SetActive(false);
var temp = _currentMenu;
_currentMenu = _nextMenu;
_nextMenu = temp;
}
}
}
private string TitleSubtitle(string title, string subtitle) => $"{title}\n<smallcaps><size=50%>{subtitle}";
private void Fade(bool fromRight)
{
_nextMenu.panel.gameObject.SetActive(true);
_nextMenu.group.alpha = 0;
_fading = true;
_fadeLerp = 0;
_fadeFromRight = fromRight;
}
private static bool _exclusionsShown;
private void ShowMain()
{
_nextMenu.panel.Clear();
_nextMenu.panel.Title.text = TitleSubtitle("aetheria", "terminus");
if (!InGame)
{
// A run exists only as the run store's SavedGame global.
var cache = ActionGameManager.CultCache;
if (ActionGameManager.ModExclusions.Length > 0 && !_exclusionsShown)
{
_exclusionsShown = true;
Refuse("Some mods were not loaded", string.Join("\n", ActionGameManager.ModExclusions.Select(exclusion => exclusion.ToString())));
}
var saved = cache.GetGlobal<SavedGame>();
if (saved == null && cache.AllStoredDocuments.Any(stored => RunSave.IsRunRecord(stored.Descriptor.DocumentType)))
Debug.Log("run store has no SavedGame; Continue disabled");
_nextMenu.panel.AddButton("Continue", saved == null ? (Action) null : () =>
{
try { RunSave.RequireDesigns(cache, saved); }
catch (InvalidOperationException error) { Refuse("Cannot continue this run", error.Message); return; }
ActionGameManager.IsTutorial = saved.IsTutorial;
ActionGameManager.CurrentGalaxy = new Galaxy(cache, saved, Debug.Log);
EnterGame();
});
}
_nextMenu.panel.AddButton("New Game",
() =>
{
RunSave.Clear(ActionGameManager.CultCache);
var generatorState = "Loading Database Contents";
Action<string> setState = s => generatorState = s;
Fade(true);
_nextMenu.panel.gameObject.SetActive(false);
Dialog.Clear();
Dialog.Title.text = "Generating Galaxy";
Dialog.AddProperty(() => generatorState);
Dialog.Show();
if (ActionGameManager.PlayerSettings.TutorialPassed)
{
var backgroundSettings = MessagePackSerializer.Deserialize<SectorBackgroundSettings>(
MessagePackSerializer.Serialize(Settings.SectorBackgroundSettings));
backgroundSettings.NoisePosition = Random.value * 1000;
ActionGameManager.IsTutorial = false;
Task.Run(() =>
{
var sector = new Galaxy(
Settings.SectorGenerationSettings,
backgroundSettings,
Settings.NameGeneratorSettings,
ActionGameManager.CultCache,
Debug.Log,
setState);
Observable.NextFrame().Subscribe(_ =>
{
ActionGameManager.CurrentGalaxy = sector;
EnterGame();
});
}).ContinueWith(t => Debug.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
else
{
var backgroundSettings = MessagePackSerializer.Deserialize<SectorBackgroundSettings>(
MessagePackSerializer.Serialize(Settings.TutorialBackgroundSettings));
int iteration = 1;
do
{
backgroundSettings.NoisePosition = Random.value * 1000;
setState($"Finding Galaxy Position: iteration {iteration++}");
} while (backgroundSettings.CloudDensity(float2(0.5f)) < .5f);
ActionGameManager.IsTutorial = true;
Task.Run(() =>
{
var sector = new Galaxy(
Settings.TutorialGenerationSettings,
backgroundSettings,
Settings.NameGeneratorSettings,
ActionGameManager.CultCache,
ActionGameManager.PlayerSettings,
ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"),
Debug.Log,
setState);
Observable.NextFrame().Subscribe(_ =>
{
ActionGameManager.CurrentGalaxy = sector;
EnterGame();
});
}).ContinueWith(t => Debug.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
});
_nextMenu.panel.AddButton("Settings",
() =>
{
ShowSettings();
Fade(true);
});
_nextMenu.panel.AddButton("Quit", Application.Quit);
}
// The one way into the game scene. Mod ship prototypes import asynchronously at boot, so the scene waits for them,
// and a preload that could not run at all stops here with its reason rather than in the first zone.
private void EnterGame() => StartCoroutine(EnterGameWhenModShipsAreReady());
private IEnumerator EnterGameWhenModShipsAreReady()
{
var loading = ShipModShips.Loading;
while (!loading.IsCompleted) yield return null;
if (loading.IsFaulted)
{
Refuse("Mod ships failed to load", loading.Exception.GetBaseException().Message);
yield break;
}
SceneManager.LoadScene("ARPG");
}
private void Refuse(string title, string reason)
{
Debug.LogError($"{title}: {reason}");
Dialog.Clear();
Dialog.Title.text = title;
Dialog.AddProperty(() => reason);
Dialog.Show(onCancel: () => { }, cancelText: "OK");
}
private void ShowSettings()
{
_nextMenu.panel.Clear();
_nextMenu.panel.Title.text = "settings";
_nextMenu.panel.AddButton("Gameplay",
() =>
{
ShowGameplaySettings();
Fade(true);
});
_nextMenu.panel.AddButton("Graphics",
() =>
{
ShowGraphicsSettings();
Fade(true);
});
_nextMenu.panel.AddButton("Input",
() =>
{
ShowInputSettings();
Fade(true);
});
_nextMenu.panel.AddButton("Audio",
() =>
{
ShowAudioSettings();
Fade(true);
});
_nextMenu.panel.AddButton("Back",
() =>
{
ShowMain();
Fade(false);
});
}
private void ShowGameplaySettings()
{
_nextMenu.panel.Clear();
_nextMenu.panel.Title.text = TitleSubtitle("gameplay", "settings");
_nextMenu.panel.AddField("Name",
() => ActionGameManager.PlayerSettings.Name,
s => ActionGameManager.PlayerSettings.Name = s);
_nextMenu.panel.AddField("Temperature Unit",
() => (int) ActionGameManager.PlayerSettings.GameplaySettings.TemperatureUnit,
i => ActionGameManager.PlayerSettings.GameplaySettings.TemperatureUnit = (TemperatureUnit) i,
Enum.GetNames(typeof(TemperatureUnit)));
_nextMenu.panel.AddField("Significant Digits",
() => ActionGameManager.PlayerSettings.GameplaySettings.SignificantDigits,
i => ActionGameManager.PlayerSettings.GameplaySettings.SignificantDigits = i);
_nextMenu.panel.AddButton("Back",
() =>
{
ActionGameManager.SavePlayerSettings();
ShowSettings();
Fade(false);
});
}
private void ShowGraphicsSettings()
{
_nextMenu.panel.Clear();
_nextMenu.panel.Title.text = TitleSubtitle("graphics", "settings");
_nextMenu.panel.AddField("Nebula Quality",
() => (int)ActionGameManager.PlayerSettings.GraphicsSettings.NebulaQuality,
i =>
{
ActionGameManager.PlayerSettings.GraphicsSettings.NebulaQuality = (Quality)i;
CloudRenderer.quality = ActionGameManager.PlayerSettings.GraphicsSettings.NebulaQuality;
},
Enum.GetNames(typeof(Quality)));
_nextMenu.panel.AddField("Show Asteroids in Minimap",
() => ActionGameManager.PlayerSettings.GraphicsSettings.ShowAsteroidsInMinimap,
b => ActionGameManager.PlayerSettings.GraphicsSettings.ShowAsteroidsInMinimap = b);
_nextMenu.panel.AddButton("Back",
() =>
{
ShowSettings();
Fade(false);
});
}
private void ShowInputSettings()
{
_nextMenu.panel.Clear();
_nextMenu.panel.Title.text = TitleSubtitle("input", "settings");
_nextMenu.panel.AddButton("Back",
() =>
{
ShowSettings();
Fade(false);
});
}
private void ShowAudioSettings()
{
_nextMenu.panel.Clear();
_nextMenu.panel.Title.text = TitleSubtitle("audio", "settings");
_nextMenu.panel.AddButton("Back",
() =>
{
ShowSettings();
Fade(false);
});
}
}