using System; using Luneraria.Core; using Luneraria.Net; using Luneraria.View; using UnityEngine; using UnityEngine.UIElements; namespace Luneraria.UI { /// /// Owns the session and the TCP link (port of lune_main.gd): title menu, match, return, /// rematch. Offline is a local host with bot-filled seats. Host/Join uses LuneSession over /// LuneTcp. Every setup from the host (first join, rejoin, rematch) relaunches the match. /// The scene builder puts it next to the game controller, so the start screen comes first. /// Tests and the benchmark keep the scene's own match (LuneMatchDriver.DirectStart, /// LuneBenchmark.Running); then the shell stays out and LuneGame starts as before U4. /// [DefaultExecutionOrder(-50)] public sealed class LuneNetPlay : MonoBehaviour { /// The start screen draws above the HUD (sorting order 10). public const int MenuSortingOrder = 20; public LuneGame Game; public LuneMatchDriver Driver; public LuneMatchView View; public LuneCameraRig Rig; public LuneUiAssets UiAssets; public LuneSession Session { get; private set; } public LuneMainMenu Menu { get; private set; } /// /// The start screen's own document. The HUD takes over the UIDocument of the match object /// and clears it on every launch, so the menu cannot live there. /// public UIDocument MenuDocument { get; private set; } LuneTcp _tcp; MatchOptions _lastOptions; bool _relaunch; PanelSettings _panel; void Awake() { if (Game == null) Game = GetComponent(); if (Driver == null) Driver = GetComponent(); if (View == null) View = GetComponent(); if (Rig == null) Rig = GetComponent(); if (UiAssets == null && Game != null) UiAssets = Game.UiAssets; } void Start() { if (LuneMatchDriver.DirectStart || LuneBenchmark.Running) { enabled = false; return; } BuildMenu(); if (Game != null) { Game.Net = this; Game.enabled = false; } if (Driver != null) Driver.enabled = false; ShowMenu(); LuneSmoke.StartIfRequested(this); } void BuildMenu() { LuneUi.Assets = UiAssets; _panel = ScriptableObject.CreateInstance(); _panel.name = "LuneMenuPanel"; _panel.themeStyleSheet = UiAssets != null ? UiAssets.Theme : null; _panel.scaleMode = PanelScaleMode.ScaleWithScreenSize; _panel.referenceResolution = LuneHud.Reference; _panel.screenMatchMode = PanelScreenMatchMode.Expand; _panel.sortingOrder = MenuSortingOrder; var holder = new GameObject("Menu"); holder.transform.SetParent(transform, false); MenuDocument = holder.AddComponent(); MenuDocument.panelSettings = _panel; // While a match runs the menu is hidden; its root must not take the HUD's clicks. MenuDocument.rootVisualElement.pickingMode = PickingMode.Ignore; Menu = new LuneMainMenu(); MenuDocument.rootVisualElement.Add(Menu); Menu.StartOffline += StartOffline; Menu.HostRequested += HostMatch; Menu.JoinRequested += JoinMatch; Menu.QuitRequested += Quit; } /// The start screen over the empty map; the camera rests, the fields take W, A, S, D, Q and E. void ShowMenu() { Menu.Enter(); if (Rig != null) { Rig.Interactive = false; Rig.SetYaw(0f); Rig.Overview(true); } } void Update() { if (_tcp != null) _tcp.Pump(); if (_relaunch) { _relaunch = false; Launch(); } } void StartOffline(MatchOptions options) { ResetSession(); Session = new LuneSession(); Session.HostOffline(options.HumanSlot, options); _lastOptions = options; Menu.Leave(); Launch(); } void HostMatch(int port, string displayName) { ResetSession(); _tcp = new LuneTcp(); Session = new LuneSession { Link = _tcp }; _tcp.Session = Session; Session.SessionMessage += OnMessage; if (!_tcp.Host(port)) { Menu.Notify(_tcp.LastError, true); ResetSession(); return; } var options = new MatchOptions { Seed = Environment.TickCount & 0xffff, HumanSlot = 0 }; for (int slot = 0; slot < 6; slot++) options.Bunds.Add(slot == 0 ? Menu.Bund : DataCatalog.Factions[(slot + options.Seed) % DataCatalog.Factions.Length]); if (!Session.HostNetwork(options, displayName)) { Menu.Notify(Session.LastError, true); ResetSession(); return; } _lastOptions = options; Menu.Notify("Warte auf Gäste auf Port " + port + " …"); Menu.Leave(); Launch(); } void JoinMatch(string address, int port, string displayName) { ResetSession(); _tcp = new LuneTcp(); Session = new LuneSession { Link = _tcp }; _tcp.Session = Session; Session.SessionMessage += OnMessage; Session.HostLost += () => { string reason = Session != null ? Session.LastError : ""; ReturnToMenu(); if (Menu != null) Menu.Notify(reason, true); }; Session.MatchResetup += () => { if (Session != null && Session.MatchState != null) _relaunch = true; }; if (!Session.JoinNetwork(displayName) || !_tcp.Join(address, port)) { Menu.Notify(_tcp.LastError.Length > 0 ? _tcp.LastError : Session.LastError, true); ResetSession(); return; } Menu.Notify("Verbinde mit " + address + ":" + port + " …"); } void OnMessage(string text, bool isError) { if (Menu != null && LuneUi.Visible(Menu)) Menu.Notify(text, isError); if (Game != null && Game.Hud != null && !isError) Game.Hud.Notify(text, isError); } void Launch() { if (Game == null || Session == null || Session.MatchState == null) return; bool networked = Session.IsNetworked(); Game.Net = this; Game.Session = networked ? Session : null; Game.HumanSlot = Session.LocalSlot >= 0 ? Session.LocalSlot : 0; if (Driver != null) { Driver.enabled = true; if (networked) Driver.Adopt(Session.MatchState, Session.LocalSlot, !Session.IsAuthority(), Session); else { // Offline keeps the plain runner; the session already validated the setup path. Driver.HumanSlot = Game.HumanSlot; Driver.Bunds = _lastOptions != null && _lastOptions.Bunds != null ? _lastOptions.Bunds.ToArray() : null; Driver.Seed = _lastOptions != null ? _lastOptions.Seed : Driver.Seed; Driver.Restart(); } } if (Rig != null) { // Every match starts with an unturned camera, like Godot's new game node. Rig.SetYaw(0f, true); Rig.Interactive = true; } Game.enabled = true; Game.Launch(); } /// Host rematch: a new match with the same seats; clients relaunch on setup. public void Rematch() { if (Session == null || !Session.IsAuthority()) { if (Game != null && Game.Hud != null) Game.Hud.Notify("Eine Revanche startet der Host.", true); return; } if (_lastOptions != null) _lastOptions.Seed += 1; MatchOptions options = _lastOptions ?? new MatchOptions(); Session.HostRematch(options); _relaunch = true; } /// /// Back to the start screen. Godot frees the game node; here the HUD closes and the driver /// holds a fresh match that never runs, so the map behind the menu is empty again. /// public void ReturnToMenu() { if (Game != null) Game.Close(); if (Driver != null) { Driver.HumanSlot = -1; Driver.Restart(); Driver.enabled = false; } ResetSession(); if (Menu != null) ShowMenu(); } void ResetSession() { if (_tcp != null) { _tcp.Close(); _tcp = null; } if (Session != null) { Session.Close(); Session = null; } } void OnDestroy() { ResetSession(); if (_panel != null) Destroy(_panel); } static void Quit() { #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #else Application.Quit(); #endif } } }