champions-of-luneraria/Assets/Luneraria/Core/Net/NetModel.cs
Martin Stickelbroeck 769e622952 U4 Netzwerk: LuneSession, TCP, Host/Join, 43+12 Prüfungen
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>
2026-10-02 16:14:42 +02:00

122 lines
4.3 KiB
C#

using System.Collections.Generic;
namespace Luneraria.Core
{
/// <summary>Roles of a session (lune_session.gd Role).</summary>
public enum LuneRole { Offline, Host, Client }
/// <summary>One lobby row: peer 0 is a bot, peer 1 the host.</summary>
public sealed class LobbyEntry
{
public int Peer;
public int Slot;
public string Name = "";
public bool Ready;
public bool Bot;
public LobbyEntry Clone() { return (LobbyEntry)MemberwiseClone(); }
}
/// <summary>Result of a session intent (Godot returns a Dictionary with ok, message, pending).</summary>
public struct SessionResult
{
public bool Ok;
public string Message;
public bool Pending;
public SessionResult(bool ok, string message, bool pending = false)
{
Ok = ok;
Message = message;
Pending = pending;
}
}
/// <summary>One player row of the public state; Draft is set only for the receiving seat.</summary>
public sealed class NetPlayerRow
{
public int Slot;
public string Name = "";
public bool IsHuman;
public int Gold;
public bool Ready;
public int Earned;
public int Spent;
public int NextSeq;
public List<RosterEntry> Roster = new List<RosterEntry>();
public HeroState Hero;
public List<Booking> Bookings = new List<Booking>();
/// <summary>Private draft of the receiving seat only; null for every other seat.</summary>
public Draft Draft;
/// <summary>Undo stack size of the receiving seat (0 for others).</summary>
public int Undo;
}
/// <summary>Public state of the match for one seat (rpc_state).</summary>
public sealed class NetState
{
public Phase Phase;
public int Round;
public int Winner;
public string EndNote = "";
public Dictionary<string, double> BuildingHp = new Dictionary<string, double>();
public BossState Boss = new BossState();
public Settlement LastSettlement;
public List<int> DestroyedCores = new List<int>();
public List<NetPlayerRow> Players = new List<NetPlayerRow>();
}
/// <summary>A mobile unit as it crosses the wire (rpc_combat_begin / rpc_units_spawned).</summary>
public sealed class NetUnitRecord
{
public int Id;
public string Kind = "";
public int Team;
public int TeamIndex;
public int OwnerSlot = -1;
public string Uid = "";
public string Lane = "";
public string Zone = "";
public UnitDef Def = new UnitDef();
public double X;
public double Y;
public double Facing;
public double Hp;
public double MaxHp;
public bool Alive;
}
/// <summary>
/// Outbound session messages (the Godot RPCs). A transport or a test recorder implements this;
/// without a link the session still runs offline and for in-process host tests.
/// </summary>
public interface ILuneNetLink
{
/// <summary>Reliable message to one peer (1 = host).</summary>
void Send(int peer, string kind, object payload);
/// <summary>Unreliable ordered combat frame to one peer.</summary>
void SendFrame(int peer, int serial, double elapsed, float[] units, float[] buildingHp);
}
/// <summary>Frames carry 13 floats per mobile unit (lune_session.gd FRAME_STRIDE).</summary>
public static class LuneNet
{
public const int DefaultPort = 24571;
public const int MaxClients = 5;
public const int Protocol = 4;
/// <summary>Seconds between combat snapshots the host sends.</summary>
public const double FrameInterval = 0.05;
public const int FrameStride = 13;
public const string KHello = "hello";
public const string KSubmit = "submit";
public const string KReady = "ready";
public const string KRefused = "refused";
public const string KFeedback = "feedback";
public const string KSetup = "setup";
public const string KState = "state";
public const string KLobby = "lobby";
public const string KCombatBegin = "combat_begin";
public const string KUnitsSpawned = "units_spawned";
}
}