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>
840 lines
30 KiB
C#
840 lines
30 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using Luneraria.Core;
|
|
|
|
namespace Luneraria.Net
|
|
{
|
|
/// <summary>
|
|
/// Compact cursor codec for the session messages (the Godot RPC payloads were Variants).
|
|
/// One message is one UTF-8 line: kind TAB payload. Compound values write their fields in a
|
|
/// fixed order; the reader consumes exactly as many tokens as the shape needs, so nesting
|
|
/// never collides with separators. No engine, no JSON library.
|
|
/// </summary>
|
|
public static class LuneWire
|
|
{
|
|
static readonly NumberFormatInfo Nfi = CultureInfo.InvariantCulture.NumberFormat;
|
|
const char Sep = '\x1f';
|
|
|
|
// ---------------------------------------------------------------- encode
|
|
public static string Encode(string kind, object payload)
|
|
{
|
|
var text = new StringBuilder(kind);
|
|
text.Append('\t');
|
|
var parts = payload as object[];
|
|
if (parts != null)
|
|
{
|
|
text.Append('i');
|
|
text.Append(parts.Length.ToString(Nfi));
|
|
for (int i = 0; i < parts.Length; i++)
|
|
{
|
|
text.Append(Sep);
|
|
WriteValue(text, parts[i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
text.Append('i');
|
|
text.Append('1');
|
|
text.Append(Sep);
|
|
WriteValue(text, payload);
|
|
}
|
|
return text.ToString();
|
|
}
|
|
|
|
public static byte[] EncodeFrame(int serial, double elapsed, float[] units, float[] buildingHp)
|
|
{
|
|
using (var stream = new MemoryStream())
|
|
using (var writer = new BinaryWriter(stream))
|
|
{
|
|
writer.Write(serial);
|
|
writer.Write(elapsed);
|
|
writer.Write(units != null ? units.Length : 0);
|
|
if (units != null)
|
|
{
|
|
for (int i = 0; i < units.Length; i++)
|
|
writer.Write(units[i]);
|
|
}
|
|
writer.Write(buildingHp != null ? buildingHp.Length : 0);
|
|
if (buildingHp != null)
|
|
{
|
|
for (int i = 0; i < buildingHp.Length; i++)
|
|
writer.Write(buildingHp[i]);
|
|
}
|
|
return stream.ToArray();
|
|
}
|
|
}
|
|
|
|
static void WriteValue(StringBuilder text, object value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
text.Append('~');
|
|
return;
|
|
}
|
|
if (value is string)
|
|
{
|
|
text.Append('s');
|
|
text.Append(Escape((string)value));
|
|
return;
|
|
}
|
|
if (value is bool)
|
|
{
|
|
text.Append((bool)value ? 'B' : 'b');
|
|
return;
|
|
}
|
|
if (value is int || value is long)
|
|
{
|
|
text.Append('i');
|
|
text.Append(Convert.ToInt64(value).ToString(Nfi));
|
|
return;
|
|
}
|
|
if (value is float || value is double)
|
|
{
|
|
text.Append('f');
|
|
text.Append(Convert.ToDouble(value).ToString("R", Nfi));
|
|
return;
|
|
}
|
|
if (value is float[])
|
|
{
|
|
float[] array = (float[])value;
|
|
text.Append('a');
|
|
text.Append(array.Length.ToString(Nfi));
|
|
for (int i = 0; i < array.Length; i++)
|
|
{
|
|
text.Append(',');
|
|
text.Append(array[i].ToString("R", Nfi));
|
|
}
|
|
return;
|
|
}
|
|
if (value is Phase)
|
|
{
|
|
text.Append('i');
|
|
text.Append(((int)(Phase)value).ToString(Nfi));
|
|
return;
|
|
}
|
|
if (value is Command)
|
|
{
|
|
text.Append('C');
|
|
WriteCommand(text, (Command)value);
|
|
return;
|
|
}
|
|
if (value is MatchOptions)
|
|
{
|
|
text.Append('O');
|
|
WriteOptions(text, (MatchOptions)value);
|
|
return;
|
|
}
|
|
if (value is LobbyEntry)
|
|
{
|
|
text.Append('L');
|
|
WriteLobby(text, (LobbyEntry)value);
|
|
return;
|
|
}
|
|
if (value is List<LobbyEntry>)
|
|
{
|
|
text.Append('l');
|
|
WriteList(text, (List<LobbyEntry>)value, WriteLobby);
|
|
return;
|
|
}
|
|
if (value is NetState)
|
|
{
|
|
text.Append('S');
|
|
WriteState(text, (NetState)value);
|
|
return;
|
|
}
|
|
if (value is NetUnitRecord)
|
|
{
|
|
text.Append('U');
|
|
WriteUnit(text, (NetUnitRecord)value);
|
|
return;
|
|
}
|
|
if (value is List<NetUnitRecord>)
|
|
{
|
|
text.Append('u');
|
|
WriteList(text, (List<NetUnitRecord>)value, WriteUnit);
|
|
return;
|
|
}
|
|
throw new InvalidDataException("LuneWire cannot encode " + value.GetType().Name);
|
|
}
|
|
|
|
static void WriteList<T>(StringBuilder text, List<T> list, Action<StringBuilder, T> write)
|
|
{
|
|
// Count rides in the tag body ("l2"); each write() starts with Sep before its first field.
|
|
text.Append(list.Count.ToString(Nfi));
|
|
for (int i = 0; i < list.Count; i++)
|
|
write(text, list[i]);
|
|
}
|
|
|
|
static void WriteCommand(StringBuilder text, Command command)
|
|
{
|
|
Str(text, command.Type ?? "");
|
|
Str(text, command.TypeId);
|
|
Str(text, command.Lane);
|
|
Str(text, command.Uid);
|
|
Str(text, command.HeroId);
|
|
Str(text, command.ItemId);
|
|
Str(text, command.ChampionId);
|
|
Int(text, command.Tier);
|
|
Int(text, command.Choice);
|
|
}
|
|
|
|
static void WriteOptions(StringBuilder text, MatchOptions options)
|
|
{
|
|
Int(text, options.Seed);
|
|
Int(text, options.HumanSlot);
|
|
Str(text, options.Bunds != null ? string.Join("\x1d", options.Bunds) : "");
|
|
Str(text, options.Profiles != null ? string.Join("\x1d", options.Profiles) : "");
|
|
}
|
|
|
|
static void WriteLobby(StringBuilder text, LobbyEntry entry)
|
|
{
|
|
Int(text, entry.Peer);
|
|
Int(text, entry.Slot);
|
|
Str(text, entry.Name ?? "");
|
|
Int(text, entry.Ready ? 1 : 0);
|
|
Int(text, entry.Bot ? 1 : 0);
|
|
}
|
|
|
|
static void WriteState(StringBuilder text, NetState state)
|
|
{
|
|
Int(text, (int)state.Phase);
|
|
Int(text, state.Round);
|
|
Int(text, state.Winner);
|
|
Str(text, state.EndNote ?? "");
|
|
var hp = state.BuildingHp ?? new Dictionary<string, double>();
|
|
Int(text, hp.Count);
|
|
foreach (KeyValuePair<string, double> pair in hp)
|
|
{
|
|
Str(text, pair.Key);
|
|
Num(text, pair.Value);
|
|
}
|
|
BossState boss = state.Boss ?? new BossState();
|
|
Int(text, boss.NextAvailableRound);
|
|
Int(text, boss.SettledRound);
|
|
Int(text, boss.Claim != null ? 1 : 0);
|
|
if (boss.Claim != null)
|
|
{
|
|
Int(text, boss.Claim.Team);
|
|
Int(text, boss.Claim.WonRound);
|
|
Int(text, boss.Claim.DeployRound);
|
|
Int(text, boss.Claim.ChooserSlot);
|
|
Str(text, boss.Claim.Lane ?? "");
|
|
}
|
|
var cores = state.DestroyedCores ?? new List<int>();
|
|
Int(text, cores.Count);
|
|
foreach (int core in cores)
|
|
Int(text, core);
|
|
Settlement settle = state.LastSettlement;
|
|
Int(text, settle != null ? 1 : 0);
|
|
if (settle != null)
|
|
{
|
|
Int(text, settle.Round);
|
|
Int(text, settle.Income);
|
|
Str(text, settle.EndReason ?? "");
|
|
Num(text, settle.CombatTime);
|
|
Str(text, settle.Boss ?? "none");
|
|
int gains = settle.Gains != null ? settle.Gains.Length : 0;
|
|
Int(text, gains);
|
|
for (int i = 0; i < gains; i++)
|
|
Int(text, settle.Gains[i]);
|
|
var lost = settle.BuildingsLost ?? new List<string>();
|
|
Int(text, lost.Count);
|
|
foreach (string id in lost)
|
|
Str(text, id);
|
|
}
|
|
var players = state.Players ?? new List<NetPlayerRow>();
|
|
Int(text, players.Count);
|
|
foreach (NetPlayerRow row in players)
|
|
WritePlayer(text, row);
|
|
}
|
|
|
|
static void WritePlayer(StringBuilder text, NetPlayerRow row)
|
|
{
|
|
Int(text, row.Slot);
|
|
Str(text, row.Name ?? "");
|
|
Int(text, row.IsHuman ? 1 : 0);
|
|
Int(text, row.Gold);
|
|
Int(text, row.Ready ? 1 : 0);
|
|
Int(text, row.Earned);
|
|
Int(text, row.Spent);
|
|
Int(text, row.NextSeq);
|
|
Int(text, row.Undo);
|
|
WriteRosterList(text, row.Roster);
|
|
Int(text, row.Hero != null ? 1 : 0);
|
|
if (row.Hero != null)
|
|
WriteHero(text, row.Hero);
|
|
WriteBookingList(text, row.Bookings);
|
|
Int(text, row.Draft != null ? 1 : 0);
|
|
if (row.Draft != null)
|
|
{
|
|
Draft draft = row.Draft;
|
|
Int(text, draft.Gold);
|
|
Int(text, draft.NextSeq);
|
|
Str(text, draft.BossLane ?? "");
|
|
WriteRosterList(text, draft.Roster);
|
|
Int(text, draft.Hero != null ? 1 : 0);
|
|
if (draft.Hero != null)
|
|
WriteHero(text, draft.Hero);
|
|
WriteBookingList(text, draft.NewBookings);
|
|
}
|
|
}
|
|
|
|
static void WriteRosterList(StringBuilder text, List<RosterEntry> list)
|
|
{
|
|
var rows = list ?? new List<RosterEntry>();
|
|
Int(text, rows.Count);
|
|
foreach (RosterEntry entry in rows)
|
|
{
|
|
Str(text, entry.Uid ?? "");
|
|
Int(text, entry.Seq);
|
|
Str(text, entry.TypeId ?? "");
|
|
Str(text, entry.Lane ?? "");
|
|
Int(text, entry.Invested);
|
|
Int(text, entry.Fresh ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
static void WriteBookingList(StringBuilder text, List<Booking> list)
|
|
{
|
|
var rows = list ?? new List<Booking>();
|
|
Int(text, rows.Count);
|
|
foreach (Booking booking in rows)
|
|
{
|
|
Str(text, booking.Uid ?? "");
|
|
Int(text, booking.Seq);
|
|
Str(text, booking.ChampionId ?? "");
|
|
Str(text, booking.Lane ?? "");
|
|
Int(text, booking.Cost);
|
|
Int(text, booking.DeployRound);
|
|
}
|
|
}
|
|
|
|
static void WriteHero(StringBuilder text, HeroState hero)
|
|
{
|
|
Str(text, hero.Uid ?? "");
|
|
Int(text, hero.Seq);
|
|
Str(text, hero.HeroId ?? "");
|
|
Str(text, hero.Lane ?? "");
|
|
Int(text, hero.Xp);
|
|
Int(text, hero.Invested);
|
|
Int(text, hero.Fresh ? 1 : 0);
|
|
Str(text, hero.Items != null ? string.Join("\x1d", hero.Items) : "");
|
|
Str(text, hero.Talents != null ? string.Join("\x1d", hero.Talents) : "");
|
|
Str(text, hero.FreshItems != null ? string.Join("\x1d", hero.FreshItems) : "");
|
|
}
|
|
|
|
static void WriteUnit(StringBuilder text, NetUnitRecord unit)
|
|
{
|
|
Int(text, unit.Id);
|
|
Str(text, unit.Kind ?? "");
|
|
Int(text, unit.Team);
|
|
Int(text, unit.TeamIndex);
|
|
Int(text, unit.OwnerSlot);
|
|
Str(text, unit.Uid ?? "");
|
|
Str(text, unit.Lane ?? "");
|
|
Str(text, unit.Zone ?? "");
|
|
Num(text, unit.X);
|
|
Num(text, unit.Y);
|
|
Num(text, unit.Facing);
|
|
Num(text, unit.Hp);
|
|
Num(text, unit.MaxHp);
|
|
Int(text, unit.Alive ? 1 : 0);
|
|
UnitDef def = unit.Def ?? new UnitDef();
|
|
Str(text, def.Id ?? "");
|
|
Str(text, def.DisplayName ?? "");
|
|
Int(text, def.Cost);
|
|
Str(text, def.UpgradeId ?? "");
|
|
Num(text, def.MaxHp);
|
|
Num(text, def.Damage);
|
|
Int(text, def.AttackType);
|
|
Int(text, def.ArmorType);
|
|
Num(text, def.AttackRange);
|
|
Num(text, def.Speed);
|
|
Str(text, def.Ability ?? "");
|
|
Int(text, def.LeakValue);
|
|
Int(text, def.Bounty);
|
|
Str(text, def.AbilityDescription ?? "");
|
|
Int(text, def.IsBoss ? 1 : 0);
|
|
Int(text, def.IncomeGain);
|
|
Str(text, def.Role ?? "");
|
|
Str(text, def.HeroId ?? "");
|
|
Int(text, def.Level);
|
|
Num(text, def.ViewScale);
|
|
Num(text, def.Regen);
|
|
Num(text, def.ItemSplash);
|
|
Num(text, def.DamageBonus);
|
|
Num(text, def.ReductionBonus);
|
|
Num(text, def.AttackSpeedBonus);
|
|
Str(text, def.ChampionId ?? "");
|
|
Str(text, def.Priority ?? "");
|
|
Num(text, def.SiegeFactor);
|
|
Num(text, def.HpFactor);
|
|
Str(text, def.Tier ?? "");
|
|
Str(text, def.Talents != null ? string.Join("\x1d", def.Talents) : "");
|
|
}
|
|
|
|
static void Str(StringBuilder text, string value)
|
|
{
|
|
text.Append(Sep);
|
|
if (value == null)
|
|
text.Append('~');
|
|
else
|
|
{
|
|
text.Append('s');
|
|
text.Append(Escape(value));
|
|
}
|
|
}
|
|
|
|
static void Int(StringBuilder text, int? value)
|
|
{
|
|
text.Append(Sep);
|
|
if (value == null)
|
|
text.Append('~');
|
|
else
|
|
{
|
|
text.Append('i');
|
|
text.Append(value.Value.ToString(Nfi));
|
|
}
|
|
}
|
|
|
|
static void Num(StringBuilder text, double value)
|
|
{
|
|
text.Append(Sep);
|
|
text.Append('f');
|
|
text.Append(value.ToString("R", Nfi));
|
|
}
|
|
|
|
static string Escape(string value)
|
|
{
|
|
return value.Replace("\\", "\\\\").Replace("\n", "\\n").Replace("\t", "\\t")
|
|
.Replace("\x1d", "\\r").Replace("\x1e", "\\e").Replace("\x1f", "\\f");
|
|
}
|
|
|
|
static string Unescape(string value)
|
|
{
|
|
// Order matters: the two-character escapes first, the literal backslash last.
|
|
return value.Replace("\\f", "\x1f").Replace("\\e", "\x1e").Replace("\\r", "\x1d")
|
|
.Replace("\\t", "\t").Replace("\\n", "\n").Replace("\\\\", "\\");
|
|
}
|
|
|
|
// ---------------------------------------------------------------- decode
|
|
public static void Decode(string line, out string kind, out object payload)
|
|
{
|
|
int tab = line.IndexOf('\t');
|
|
kind = tab < 0 ? line : line.Substring(0, tab);
|
|
string body = tab < 0 ? "" : line.Substring(tab + 1);
|
|
if (body.Length == 0)
|
|
{
|
|
payload = null;
|
|
return;
|
|
}
|
|
var reader = new Reader(body);
|
|
// Leading token is the top-level value count (1 = single payload, n = object[]).
|
|
int count = reader.Integer(1);
|
|
if (count <= 1)
|
|
{
|
|
payload = reader.Value();
|
|
return;
|
|
}
|
|
var parts = new object[count];
|
|
for (int i = 0; i < count; i++)
|
|
parts[i] = reader.Value();
|
|
payload = parts;
|
|
}
|
|
|
|
public static void DecodeFrame(byte[] buffer, out int serial, out double elapsed, out float[] units, out float[] buildingHp)
|
|
{
|
|
using (var stream = new MemoryStream(buffer))
|
|
using (var reader = new BinaryReader(stream))
|
|
{
|
|
serial = reader.ReadInt32();
|
|
elapsed = reader.ReadDouble();
|
|
int unitCount = reader.ReadInt32();
|
|
units = new float[unitCount];
|
|
for (int i = 0; i < unitCount; i++)
|
|
units[i] = reader.ReadSingle();
|
|
int hpCount = reader.ReadInt32();
|
|
buildingHp = new float[hpCount];
|
|
for (int i = 0; i < hpCount; i++)
|
|
buildingHp[i] = reader.ReadSingle();
|
|
}
|
|
}
|
|
|
|
/// <summary>Token cursor over a Sep-separated payload. Tagged values: ~ null, s string, i int, f float, B/b bool, a float[], C/O/L/l/S/U/u compounds.</summary>
|
|
sealed class Reader
|
|
{
|
|
readonly string[] _tokens;
|
|
int _at;
|
|
|
|
public Reader(string body)
|
|
{
|
|
_tokens = body.Split(Sep);
|
|
_at = 0;
|
|
}
|
|
|
|
public int Remaining { get { return _tokens.Length - _at; } }
|
|
|
|
string Take()
|
|
{
|
|
if (_at >= _tokens.Length)
|
|
throw new InvalidDataException("LuneWire: payload ends early");
|
|
return _tokens[_at++];
|
|
}
|
|
|
|
public object Value()
|
|
{
|
|
string token = Take();
|
|
if (token.Length == 0 || token[0] == '~')
|
|
return null;
|
|
char tag = token[0];
|
|
string body = token.Substring(1);
|
|
switch (tag)
|
|
{
|
|
case 's':
|
|
return Unescape(body);
|
|
case 'B':
|
|
return true;
|
|
case 'b':
|
|
return false;
|
|
case 'i':
|
|
return int.Parse(body, Nfi);
|
|
case 'f':
|
|
return double.Parse(body, Nfi);
|
|
case 'a':
|
|
{
|
|
string[] parts = body.Split(',');
|
|
var data = new float[Math.Max(0, parts.Length - 1)];
|
|
for (int i = 1; i < parts.Length; i++)
|
|
data[i - 1] = float.Parse(parts[i], Nfi);
|
|
return data;
|
|
}
|
|
case 'C':
|
|
return ReadCommand();
|
|
case 'O':
|
|
return ReadOptions();
|
|
case 'L':
|
|
return ReadLobby();
|
|
case 'l':
|
|
return ReadLobbyList(body);
|
|
case 'S':
|
|
return ReadState();
|
|
case 'U':
|
|
return ReadUnit();
|
|
case 'u':
|
|
return ReadUnitList(body);
|
|
default:
|
|
throw new InvalidDataException("LuneWire tag " + tag);
|
|
}
|
|
}
|
|
|
|
string Text()
|
|
{
|
|
object value = Value();
|
|
return value as string;
|
|
}
|
|
|
|
public int Integer(int fallback = 0)
|
|
{
|
|
object value = Value();
|
|
if (value == null)
|
|
return fallback;
|
|
return Convert.ToInt32(value);
|
|
}
|
|
|
|
double Number(double fallback = 0.0)
|
|
{
|
|
object value = Value();
|
|
if (value == null)
|
|
return fallback;
|
|
return Convert.ToDouble(value);
|
|
}
|
|
|
|
bool Flag()
|
|
{
|
|
object value = Value();
|
|
if (value == null)
|
|
return false;
|
|
if (value is bool)
|
|
return (bool)value;
|
|
return Convert.ToInt32(value) != 0;
|
|
}
|
|
|
|
Command ReadCommand()
|
|
{
|
|
return new Command
|
|
{
|
|
Type = Text() ?? "",
|
|
TypeId = Text(),
|
|
Lane = Text(),
|
|
Uid = Text(),
|
|
HeroId = Text(),
|
|
ItemId = Text(),
|
|
ChampionId = Text(),
|
|
Tier = ReadNullableInt(),
|
|
Choice = ReadNullableInt()
|
|
};
|
|
}
|
|
|
|
int? ReadNullableInt()
|
|
{
|
|
object value = Value();
|
|
if (value == null)
|
|
return null;
|
|
return Convert.ToInt32(value);
|
|
}
|
|
|
|
MatchOptions ReadOptions()
|
|
{
|
|
var options = new MatchOptions { Seed = Integer(1), HumanSlot = Integer(1) };
|
|
string bunds = Text();
|
|
if (!string.IsNullOrEmpty(bunds))
|
|
options.Bunds.AddRange(bunds.Split('\x1d'));
|
|
string profiles = Text();
|
|
if (!string.IsNullOrEmpty(profiles))
|
|
options.Profiles.AddRange(profiles.Split('\x1d'));
|
|
return options;
|
|
}
|
|
|
|
LobbyEntry ReadLobby()
|
|
{
|
|
return new LobbyEntry
|
|
{
|
|
Peer = Integer(),
|
|
Slot = Integer(),
|
|
Name = Text() ?? "",
|
|
Ready = Flag(),
|
|
Bot = Flag()
|
|
};
|
|
}
|
|
|
|
List<LobbyEntry> ReadLobbyList(string body)
|
|
{
|
|
int count = body.Length > 0 ? int.Parse(body, Nfi) : 0;
|
|
var list = new List<LobbyEntry>(count);
|
|
for (int i = 0; i < count; i++)
|
|
list.Add(ReadLobby());
|
|
return list;
|
|
}
|
|
|
|
NetState ReadState()
|
|
{
|
|
var state = new NetState
|
|
{
|
|
Phase = (Phase)Integer(),
|
|
Round = Integer(),
|
|
Winner = Integer(),
|
|
EndNote = Text() ?? ""
|
|
};
|
|
int hpCount = Integer();
|
|
for (int i = 0; i < hpCount; i++)
|
|
{
|
|
string id = Text() ?? "";
|
|
state.BuildingHp[id] = Number();
|
|
}
|
|
state.Boss = new BossState { NextAvailableRound = Integer(), SettledRound = Integer() };
|
|
if (Flag())
|
|
{
|
|
state.Boss.Claim = new BossClaim
|
|
{
|
|
Team = Integer(),
|
|
WonRound = Integer(),
|
|
DeployRound = Integer(),
|
|
ChooserSlot = Integer(),
|
|
Lane = Text() ?? ""
|
|
};
|
|
}
|
|
int coreCount = Integer();
|
|
for (int i = 0; i < coreCount; i++)
|
|
state.DestroyedCores.Add(Integer());
|
|
if (Flag())
|
|
{
|
|
var settle = new Settlement
|
|
{
|
|
Round = Integer(),
|
|
Income = Integer(),
|
|
EndReason = Text() ?? "",
|
|
CombatTime = Number(),
|
|
Boss = Text() ?? "none"
|
|
};
|
|
int gains = Integer();
|
|
for (int i = 0; i < gains && i < settle.Gains.Length; i++)
|
|
settle.Gains[i] = Integer();
|
|
int lost = Integer();
|
|
for (int i = 0; i < lost; i++)
|
|
settle.BuildingsLost.Add(Text() ?? "");
|
|
state.LastSettlement = settle;
|
|
}
|
|
int players = Integer();
|
|
for (int i = 0; i < players; i++)
|
|
state.Players.Add(ReadPlayer());
|
|
return state;
|
|
}
|
|
|
|
NetPlayerRow ReadPlayer()
|
|
{
|
|
var row = new NetPlayerRow
|
|
{
|
|
Slot = Integer(),
|
|
Name = Text() ?? "",
|
|
IsHuman = Flag(),
|
|
Gold = Integer(),
|
|
Ready = Flag(),
|
|
Earned = Integer(),
|
|
Spent = Integer(),
|
|
NextSeq = Integer(),
|
|
Undo = Integer()
|
|
};
|
|
row.Roster = ReadRosterList();
|
|
if (Flag())
|
|
row.Hero = ReadHero();
|
|
row.Bookings = ReadBookingList();
|
|
if (Flag())
|
|
{
|
|
var draft = new Draft { Gold = Integer(), NextSeq = Integer(), BossLane = Text() ?? "" };
|
|
draft.Roster = ReadRosterList();
|
|
if (Flag())
|
|
draft.Hero = ReadHero();
|
|
draft.NewBookings = ReadBookingList();
|
|
row.Draft = draft;
|
|
}
|
|
return row;
|
|
}
|
|
|
|
List<RosterEntry> ReadRosterList()
|
|
{
|
|
int count = Integer();
|
|
var list = new List<RosterEntry>(count);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
list.Add(new RosterEntry
|
|
{
|
|
Uid = Text() ?? "",
|
|
Seq = Integer(),
|
|
TypeId = Text() ?? "",
|
|
Lane = Text() ?? "",
|
|
Invested = Integer(),
|
|
Fresh = Flag()
|
|
});
|
|
}
|
|
return list;
|
|
}
|
|
|
|
List<Booking> ReadBookingList()
|
|
{
|
|
int count = Integer();
|
|
var list = new List<Booking>(count);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
list.Add(new Booking
|
|
{
|
|
Uid = Text() ?? "",
|
|
Seq = Integer(),
|
|
ChampionId = Text() ?? "",
|
|
Lane = Text() ?? "",
|
|
Cost = Integer(),
|
|
DeployRound = Integer()
|
|
});
|
|
}
|
|
return list;
|
|
}
|
|
|
|
HeroState ReadHero()
|
|
{
|
|
var hero = new HeroState
|
|
{
|
|
Uid = Text() ?? "",
|
|
Seq = Integer(),
|
|
HeroId = Text() ?? "",
|
|
Lane = Text() ?? "",
|
|
Xp = Integer(),
|
|
Invested = Integer(),
|
|
Fresh = Flag()
|
|
};
|
|
AddSplit(hero.Items, Text());
|
|
AddSplit(hero.Talents, Text());
|
|
AddSplit(hero.FreshItems, Text());
|
|
return hero;
|
|
}
|
|
|
|
static void AddSplit(List<string> target, string value)
|
|
{
|
|
target.Clear();
|
|
if (string.IsNullOrEmpty(value))
|
|
return;
|
|
foreach (string part in value.Split('\x1d'))
|
|
{
|
|
if (part.Length > 0)
|
|
target.Add(part);
|
|
}
|
|
}
|
|
|
|
NetUnitRecord ReadUnit()
|
|
{
|
|
var unit = new NetUnitRecord
|
|
{
|
|
Id = Integer(),
|
|
Kind = Text() ?? "",
|
|
Team = Integer(),
|
|
TeamIndex = Integer(),
|
|
OwnerSlot = Integer(),
|
|
Uid = Text() ?? "",
|
|
Lane = Text() ?? "",
|
|
Zone = Text() ?? "",
|
|
X = Number(),
|
|
Y = Number(),
|
|
Facing = Number(),
|
|
Hp = Number(),
|
|
MaxHp = Number(),
|
|
Alive = Flag(),
|
|
Def = new UnitDef
|
|
{
|
|
Id = Text() ?? "",
|
|
DisplayName = Text() ?? "",
|
|
Cost = Integer(),
|
|
UpgradeId = Text() ?? "",
|
|
MaxHp = Number(1.0),
|
|
Damage = Number(),
|
|
AttackType = Integer(),
|
|
ArmorType = Integer(),
|
|
AttackRange = Number(1.0),
|
|
Speed = Number(2.0),
|
|
Ability = Text() ?? "",
|
|
LeakValue = Integer(),
|
|
Bounty = Integer(),
|
|
AbilityDescription = Text() ?? "",
|
|
IsBoss = Flag(),
|
|
IncomeGain = Integer(),
|
|
Role = Text() ?? "",
|
|
HeroId = Text() ?? "",
|
|
Level = Integer(1),
|
|
ViewScale = Number(1.0),
|
|
Regen = Number(),
|
|
ItemSplash = Number(),
|
|
DamageBonus = Number(),
|
|
ReductionBonus = Number(),
|
|
AttackSpeedBonus = Number(),
|
|
ChampionId = Text() ?? "",
|
|
Priority = Text() ?? "",
|
|
SiegeFactor = Number(1.0),
|
|
HpFactor = Number(1.0),
|
|
Tier = Text() ?? ""
|
|
}
|
|
};
|
|
AddSplit(unit.Def.Talents, Text());
|
|
return unit;
|
|
}
|
|
|
|
List<NetUnitRecord> ReadUnitList(string body)
|
|
{
|
|
int count = body.Length > 0 ? int.Parse(body, Nfi) : 0;
|
|
var list = new List<NetUnitRecord>(count);
|
|
for (int i = 0; i < count; i++)
|
|
list.Add(ReadUnit());
|
|
return list;
|
|
}
|
|
}
|
|
}
|
|
}
|