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. /// [DefaultExecutionOrder(-50)] public sealed class LuneNetPlay : MonoBehaviour { 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; } LuneTcp _tcp; MatchOptions _lastOptions; bool _relaunch; UIDocument _document; void Awake() { if (Game == null) Game = GetComponent(); if (Driver == null) Driver = GetComponent(); if (View == null) View = GetComponent(); if (Rig == null) Rig = GetComponent(); _document = GetComponent(); } void Start() { var root = new VisualElement(); root.style.position = Position.Absolute; root.style.left = 0f; root.style.top = 0f; root.style.right = 0f; root.style.bottom = 0f; if (_document != null && _document.rootVisualElement != null) { _document.rootVisualElement.Add(root); } else { // Tests and captures build their own panel; a runtime fallback still shows the menu. root.name = "lune-menu-root"; gameObject.AddComponent(); } Menu = new LuneMainMenu(); root.Add(Menu); Menu.StartOffline += StartOffline; Menu.HostRequested += HostMatch; Menu.JoinRequested += JoinMatch; Menu.QuitRequested += Quit; Menu.Enter(); if (Game != null) { Game.Net = this; Game.enabled = false; } if (Driver != null) Driver.enabled = false; } 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(); } } 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; } public void ReturnToMenu() { if (Game != null) Game.enabled = false; if (Driver != null) Driver.enabled = false; ResetSession(); if (Menu != null) Menu.Enter(); } void ResetSession() { if (_tcp != null) { _tcp.Close(); _tcp = null; } if (Session != null) { Session.Close(); Session = null; } } void OnDestroy() { ResetSession(); } static void Quit() { #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #else Application.Quit(); #endif } } }