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>
170 lines
7.4 KiB
C#
170 lines
7.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Luneraria.Core;
|
|
using Luneraria.Net;
|
|
using NUnit.Framework;
|
|
|
|
namespace Luneraria.Net.Tests
|
|
{
|
|
/// <summary>Round-trip checks for the wire codec (LuneWire) and a localhost TCP session.</summary>
|
|
public sealed class LuneWireTests
|
|
{
|
|
[Test]
|
|
public void CommandRoundTrips()
|
|
{
|
|
Command original = Command.Buy("g01", "mid");
|
|
string line = LuneWire.Encode(LuneNet.KSubmit, new object[] { 3, original });
|
|
string kind;
|
|
object payload;
|
|
LuneWire.Decode(line, out kind, out payload);
|
|
Assert.AreEqual(LuneNet.KSubmit, kind);
|
|
var parts = (object[])payload;
|
|
Assert.AreEqual(3, parts[0]);
|
|
var copy = (Command)parts[1];
|
|
Assert.AreEqual("buy", copy.Type);
|
|
Assert.AreEqual("g01", copy.TypeId);
|
|
Assert.AreEqual("mid", copy.Lane);
|
|
}
|
|
|
|
[Test]
|
|
public void StateRoundTrips()
|
|
{
|
|
var state = new NetState
|
|
{
|
|
Phase = Phase.Planning,
|
|
Round = 7,
|
|
Winner = LuneMatch.Ongoing,
|
|
EndNote = "Ancient von Blau zerstört.",
|
|
LastSettlement = new Settlement { Round = 6, Income = 90, EndReason = "cleared", CombatTime = 12.5, Boss = "won" }
|
|
};
|
|
state.BuildingHp["blau_ancient"] = 1200.5;
|
|
state.BuildingHp["rot_top_outer"] = 0.0;
|
|
state.DestroyedCores.Add(1);
|
|
state.Boss.NextAvailableRound = 4;
|
|
state.Boss.Claim = new BossClaim { Team = 0, WonRound = 3, DeployRound = 5, ChooserSlot = 1, Lane = "mid" };
|
|
var row = new NetPlayerRow
|
|
{
|
|
Slot = 1,
|
|
Name = "Anna",
|
|
IsHuman = true,
|
|
Gold = 340,
|
|
Ready = false,
|
|
Earned = 400,
|
|
Spent = 60,
|
|
NextSeq = 4,
|
|
Undo = 1,
|
|
Draft = new Draft { Gold = 300, NextSeq = 4, BossLane = "top" }
|
|
};
|
|
row.Roster.Add(new RosterEntry { Uid = "p1-1", Seq = 1, TypeId = "w01", Lane = "mid", Invested = 50, Fresh = true });
|
|
row.Hero = new HeroState { Uid = "p1-2", Seq = 2, HeroId = "held_ringborke", Lane = "mid", Xp = 12, Invested = 200, Fresh = false };
|
|
row.Hero.Items.Add("glutklinge");
|
|
row.Bookings.Add(new Booking { Uid = "p1-3", Seq = 3, ChampionId = "ch_belagerung", Lane = "bot", Cost = 80, DeployRound = 8 });
|
|
row.Draft.Roster.Add(new RosterEntry { Uid = "p1-4", Seq = 4, TypeId = "g01", Lane = "top", Invested = 45, Fresh = true });
|
|
row.Draft.NewBookings.Add(new Booking { Uid = "p1-5", Seq = 5, ChampionId = "ch_sturm", Lane = "mid", Cost = 70 });
|
|
state.Players.Add(row);
|
|
|
|
string line = LuneWire.Encode(LuneNet.KState, state);
|
|
string kind;
|
|
object payload;
|
|
LuneWire.Decode(line, out kind, out payload);
|
|
var copy = (NetState)payload;
|
|
Assert.AreEqual(Phase.Planning, copy.Phase);
|
|
Assert.AreEqual(7, copy.Round);
|
|
Assert.AreEqual("Ancient von Blau zerstört.", copy.EndNote);
|
|
Assert.AreEqual(1200.5, copy.BuildingHp["blau_ancient"], 1e-9);
|
|
Assert.AreEqual(0.0, copy.BuildingHp["rot_top_outer"], 1e-9);
|
|
Assert.AreEqual(1, copy.DestroyedCores[0]);
|
|
Assert.AreEqual(4, copy.Boss.NextAvailableRound);
|
|
Assert.AreEqual("mid", copy.Boss.Claim.Lane);
|
|
Assert.AreEqual(12.5, copy.LastSettlement.CombatTime, 1e-9);
|
|
NetPlayerRow player = copy.Players[0];
|
|
Assert.AreEqual("Anna", player.Name);
|
|
Assert.AreEqual(340, player.Gold);
|
|
Assert.AreEqual("p1-1", player.Roster[0].Uid);
|
|
Assert.IsTrue(player.Roster[0].Fresh);
|
|
Assert.AreEqual("held_ringborke", player.Hero.HeroId);
|
|
Assert.AreEqual("glutklinge", player.Hero.Items[0]);
|
|
Assert.AreEqual("ch_belagerung", player.Bookings[0].ChampionId);
|
|
Assert.AreEqual(300, player.Draft.Gold);
|
|
Assert.AreEqual("top", player.Draft.BossLane);
|
|
Assert.AreEqual("g01", player.Draft.Roster[0].TypeId);
|
|
Assert.AreEqual("ch_sturm", player.Draft.NewBookings[0].ChampionId);
|
|
}
|
|
|
|
[Test]
|
|
public void UnitsAndFrameRoundTrip()
|
|
{
|
|
var unit = new NetUnitRecord
|
|
{
|
|
Id = 12,
|
|
Kind = "hero",
|
|
Team = 0,
|
|
TeamIndex = 2,
|
|
OwnerSlot = 1,
|
|
Uid = "p1-2",
|
|
Lane = "mid",
|
|
Zone = "lane",
|
|
X = 3.5,
|
|
Y = -1.25,
|
|
Facing = 1.5,
|
|
Hp = 80,
|
|
MaxHp = 100,
|
|
Alive = true,
|
|
Def = new UnitDef { Id = "held_ringborke", DisplayName = "Ringborke", Cost = 200, MaxHp = 100, Damage = 12, Tier = "" }
|
|
};
|
|
unit.Def.Talents.Add("t_roots");
|
|
string line = LuneWire.Encode(LuneNet.KUnitsSpawned, new List<NetUnitRecord> { unit });
|
|
string kind;
|
|
object payload;
|
|
LuneWire.Decode(line, out kind, out payload);
|
|
var list = (List<NetUnitRecord>)payload;
|
|
Assert.AreEqual(1, list.Count);
|
|
Assert.AreEqual("hero", list[0].Kind);
|
|
Assert.AreEqual(3.5, list[0].X, 1e-9);
|
|
Assert.AreEqual("held_ringborke", list[0].Def.Id);
|
|
Assert.AreEqual("t_roots", list[0].Def.Talents[0]);
|
|
|
|
var floats = new float[LuneNet.FrameStride];
|
|
for (int i = 0; i < floats.Length; i++)
|
|
floats[i] = i + 0.5f;
|
|
byte[] frame = LuneWire.EncodeFrame(9, 1.25, floats, new[] { 10f, 0f });
|
|
int serial;
|
|
double elapsed;
|
|
float[] units;
|
|
float[] hp;
|
|
LuneWire.DecodeFrame(frame, out serial, out elapsed, out units, out hp);
|
|
Assert.AreEqual(9, serial);
|
|
Assert.AreEqual(1.25, elapsed, 1e-9);
|
|
Assert.AreEqual(LuneNet.FrameStride, units.Length);
|
|
Assert.AreEqual(2.5f, units[2]);
|
|
Assert.AreEqual(2, hp.Length);
|
|
}
|
|
|
|
[Test]
|
|
public void LobbyAndOptionsRoundTrip()
|
|
{
|
|
var roster = new List<LobbyEntry>
|
|
{
|
|
new LobbyEntry { Peer = 1, Slot = 0, Name = "Host", Ready = false, Bot = false },
|
|
new LobbyEntry { Peer = 0, Slot = 1, Name = "Bot", Ready = true, Bot = true }
|
|
};
|
|
string line = LuneWire.Encode(LuneNet.KLobby, roster);
|
|
string kind;
|
|
object payload;
|
|
LuneWire.Decode(line, out kind, out payload);
|
|
var copy = (List<LobbyEntry>)payload;
|
|
Assert.AreEqual(2, copy.Count);
|
|
Assert.AreEqual("Host", copy[0].Name);
|
|
Assert.IsTrue(copy[1].Bot);
|
|
|
|
var options = new MatchOptions { Seed = 42, HumanSlot = 0, Bunds = new List<string> { "glutpakt", "wurzelbund" } };
|
|
line = LuneWire.Encode(LuneNet.KSetup, new object[] { options, 3, roster });
|
|
LuneWire.Decode(line, out kind, out payload);
|
|
var parts = (object[])payload;
|
|
var optionsCopy = (MatchOptions)parts[0];
|
|
Assert.AreEqual(42, optionsCopy.Seed);
|
|
Assert.AreEqual(3, parts[1]);
|
|
Assert.AreEqual(2, optionsCopy.Bunds.Count);
|
|
}
|
|
}
|
|
}
|