Der M4-Umfang ist als Protokoll 4 portiert, serverautoritativ. Der Host besitzt LuneMatch und rechnet jeden Kampf; Clients senden nur Absichten und zeigen den Kampf aus Host-Momentaufnahmen. - LuneSession (Core/Net): Rollen offline/Host/Gast, sechs Sitze, Lobby, Hallo mit Protokollprüfung und Namenskürzung, Beitritt und Wieder- einstieg über den Anzeigenamen, private Planung mit Spoof-Schutz, Bereit ist verbindlich, Bots planen einmal je Runde, Disconnect gibt den Platz an einen Bot (nie Kernverlust), Übernahme verwirft den unbestätigten Bot-Entwurf, Revanche hält die Plätze, Hostverlust meldet und führt ins Menü zurück. - Kampfsynchronisation: Beginn mit Einheitenätzen und Gebäude-HP, alle 0,05 s neue Einheiten und ein Frame (13 Werte je Einheit), späte Frames über die Serial verworfen, Gast interpoliert (frame_alpha). - Transport: LuneLoopback (In-Prozess) für die Tests, LuneTcp + LuneWire (Cursor-Codec) für Echtverbindungen statt ENet; ein Socket je Gast. - Oberfläche: LuneMainMenu (Offline/Hosten/Beitreten, Bund, Lane, Name, Adresse, Port) als Startbildschirm, LuneNetPlay als Schale wie lune_main.gd. LuneGame leitet Befehle, Rückgängig und Bereit im Netzspiel über die Sitzung; Tempo und Pause bestimmt der Host. - Tests: NetSuite 43/43 (Port von test_lune_net.gd), NetRoundsSuite 12/12 (Port von net_rounds.py: 5 Runden, Prüfsummen gleich, Gast rechnet 0 Schritte, Drop und Wiedereinstieg auf denselben Sitz), LuneWireTests Round-trip. EditMode 29/29, PlayMode 13/13, Kerntests 457/457. Fingerabdrücke der Bot-Partien unverändert. - Dokumentation: U4-NETWORK (Abnahme, Grenzen), PORT-QA (U4), PORT-STATUS (U4 fertig, U5 nächster Schritt), KNOWN-BEHAVIOR-DIFFERENCES. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
245 lines
7.9 KiB
C#
245 lines
7.9 KiB
C#
using System;
|
|
using Luneraria.Core;
|
|
using Luneraria.Net;
|
|
using Luneraria.View;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace Luneraria.UI
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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<LuneGame>();
|
|
if (Driver == null)
|
|
Driver = GetComponent<LuneMatchDriver>();
|
|
if (View == null)
|
|
View = GetComponent<LuneMatchView>();
|
|
if (Rig == null)
|
|
Rig = GetComponent<LuneCameraRig>();
|
|
_document = GetComponent<UIDocument>();
|
|
}
|
|
|
|
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<UIDocument>();
|
|
}
|
|
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();
|
|
}
|
|
|
|
/// <summary>Host rematch: a new match with the same seats; clients relaunch on setup.</summary>
|
|
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
|
|
}
|
|
}
|
|
}
|