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>
110 lines
4.6 KiB
C#
110 lines
4.6 KiB
C#
using System.Collections.Generic;
|
||
using Luneraria.Core;
|
||
using UnityEngine;
|
||
|
||
namespace Luneraria.View
|
||
{
|
||
/// <summary>
|
||
/// Hosts a match in the scene (U2C/U2D; human seat and tempo since U3). Update only hands frame
|
||
/// time (times the speed) to the runner's fixed clock; it never decides anything in combat.
|
||
/// Seed, start round and extra gold are the public fixtures of the golden scenarios. With
|
||
/// HumanSlot >= 0 the HUD plans that seat and planning waits for its Ready; tempo steps
|
||
/// (1×, 2×, 4×) and pause act only in combat, as in Godot (lune_game.gd).
|
||
/// </summary>
|
||
public sealed class LuneMatchDriver : MonoBehaviour
|
||
{
|
||
/// <summary>Tempo steps of the HUD's tempo button.</summary>
|
||
public static readonly int[] SpeedSteps = { 1, 2, 4 };
|
||
|
||
public int Seed = 2;
|
||
public int StartRound = 1;
|
||
public int ExtraGold;
|
||
public string Profile = "";
|
||
/// <summary>Optional Bünde of the six seats (fixtures like Godot's start option "bunds"); empty seats follow the seed.</summary>
|
||
[System.NonSerialized] public string[] Bunds;
|
||
/// <summary>The seat a human plays through the HUD; -1 for a bot match (U2 tests, benchmark).</summary>
|
||
public int HumanSlot = -1;
|
||
/// <summary>Match speed: simulated seconds per real second (tests run faster than 1).</summary>
|
||
public float Speed = 1f;
|
||
/// <summary>Index into SpeedSteps (the player's tempo, on top of Speed).</summary>
|
||
public int SpeedIndex;
|
||
public bool Paused;
|
||
public bool RecordTrace = true;
|
||
|
||
public LuneMatchRunner Runner { get; private set; }
|
||
/// <summary>Main-thread cost of the last frame's simulation (milliseconds).</summary>
|
||
public double LastAdvanceMs { get; private set; }
|
||
/// <summary>Bound network session (U4); null offline.</summary>
|
||
public LuneSession Session;
|
||
/// <summary>True on a guest: the host simulates, this side only shows snapshots.</summary>
|
||
public bool IsClient;
|
||
readonly System.Diagnostics.Stopwatch _clock = new System.Diagnostics.Stopwatch();
|
||
|
||
public int Tempo { get { return SpeedSteps[Mathf.Clamp(SpeedIndex, 0, SpeedSteps.Length - 1)]; } }
|
||
|
||
public void Restart()
|
||
{
|
||
var options = new MatchOptions { Seed = Seed, HumanSlot = HumanSlot, Bunds = Bunds != null ? new List<string>(Bunds) : new List<string>() };
|
||
Runner = new LuneMatchRunner(options, StartRound, ExtraGold, Profile);
|
||
if (RecordTrace)
|
||
Runner.Trace = new LuneTrace();
|
||
SpeedIndex = 0;
|
||
Paused = false;
|
||
Session = null;
|
||
IsClient = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Adopts the session's match (U4). The host still advances combat and ticks snapshots;
|
||
/// a guest never advances — its match is a display mirror of the host's snapshots.
|
||
/// </summary>
|
||
public void Adopt(LuneMatch match, int humanSlot, bool isClient, LuneSession session)
|
||
{
|
||
Session = session;
|
||
IsClient = isClient;
|
||
Runner = new LuneMatchRunner(match, humanSlot, session);
|
||
SpeedIndex = 0;
|
||
Paused = false;
|
||
}
|
||
|
||
/// <summary>Next tempo step (1× → 2× → 4× → 1×).</summary>
|
||
public void CycleSpeed()
|
||
{
|
||
SpeedIndex = (SpeedIndex + 1) % SpeedSteps.Length;
|
||
}
|
||
|
||
/// <summary>Pause exists only in combat; planning waits for the player anyway.</summary>
|
||
public bool TogglePause()
|
||
{
|
||
if (Runner == null || Runner.Current != LuneMatchRunner.Stage.Combat)
|
||
return false;
|
||
Paused = !Paused;
|
||
return true;
|
||
}
|
||
|
||
void Awake()
|
||
{
|
||
if (Runner == null)
|
||
Restart();
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
if (Runner == null)
|
||
return;
|
||
_clock.Restart();
|
||
// A guest shows the host's snapshots and never advances the match.
|
||
if (IsClient)
|
||
{
|
||
LastAdvanceMs = 0.0;
|
||
return;
|
||
}
|
||
// Pause holds only the combat steps (Godot: a paused game still plans and settles).
|
||
if (!(Paused && Runner.Current == LuneMatchRunner.Stage.Combat))
|
||
Runner.Advance(Time.deltaTime * Speed * Tempo);
|
||
if (Session != null && Session.IsNetworked() && Session.IsAuthority())
|
||
Session.HostTick(Time.deltaTime * Speed * Tempo);
|
||
LastAdvanceMs = _clock.Elapsed.TotalMilliseconds;
|
||
}
|
||
}
|
||
}
|