using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using Luneraria.Core; namespace Luneraria.Net { /// /// 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. /// 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) { text.Append('l'); WriteList(text, (List)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) { text.Append('u'); WriteList(text, (List)value, WriteUnit); return; } throw new InvalidDataException("LuneWire cannot encode " + value.GetType().Name); } static void WriteList(StringBuilder text, List list, Action 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) : ""); Int(text, options.Extended ? 1 : 0); } 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(); Int(text, hp.Count); foreach (KeyValuePair 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 ?? ""); } Int(text, boss.Blessing != null && boss.Blessing.Length > 0 ? boss.Blessing[0] : 0); Int(text, boss.Blessing != null && boss.Blessing.Length > 1 ? boss.Blessing[1] : 0); for (int team = 0; team < 2; team++) { Int(text, boss.GlyphReady != null && boss.GlyphReady.Length > team ? boss.GlyphReady[team] : 1); Int(text, boss.CoreLevel != null && boss.CoreLevel.Length > team ? boss.CoreLevel[team] : 0); } var cores = state.DestroyedCores ?? new List(); 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(); Int(text, lost.Count); foreach (string id in lost) Str(text, id); } var players = state.Players ?? new List(); 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); Int(text, row.IncomeBonus); 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 ?? ""); Str(text, draft.GlyphLane ?? ""); Int(text, draft.CoreInvest); 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 list) { var rows = list ?? new List(); 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 list) { var rows = list ?? new List(); 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) : ""); Str(text, hero.Ultimate ?? ""); Str(text, hero.Skin ?? ""); Int(text, hero.Exhausted ? 1 : 0); } 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) : ""); Str(text, def.UltimateRule ?? ""); Str(text, def.Skin ?? ""); } 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(); } } /// 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. 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')); options.Extended = Integer() == 1; return options; } LobbyEntry ReadLobby() { return new LobbyEntry { Peer = Integer(), Slot = Integer(), Name = Text() ?? "", Ready = Flag(), Bot = Flag() }; } List ReadLobbyList(string body) { int count = body.Length > 0 ? int.Parse(body, Nfi) : 0; var list = new List(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() ?? "" }; } state.Boss.Blessing = new[] { Integer(), Integer() }; for (int team = 0; team < 2; team++) { state.Boss.GlyphReady[team] = Integer(1); state.Boss.CoreLevel[team] = Integer(); } 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(), IncomeBonus = Integer() }; row.Roster = ReadRosterList(); if (Flag()) row.Hero = ReadHero(); row.Bookings = ReadBookingList(); if (Flag()) { var draft = new Draft { Gold = Integer(), NextSeq = Integer(), BossLane = Text() ?? "", GlyphLane = Text() ?? "", CoreInvest = Integer() }; draft.Roster = ReadRosterList(); if (Flag()) draft.Hero = ReadHero(); draft.NewBookings = ReadBookingList(); row.Draft = draft; } return row; } List ReadRosterList() { int count = Integer(); var list = new List(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 ReadBookingList() { int count = Integer(); var list = new List(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()); hero.Ultimate = Text() ?? ""; hero.Skin = Text() ?? ""; hero.Exhausted = Integer() == 1; return hero; } static void AddSplit(List 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()); unit.Def.UltimateRule = Text() ?? ""; unit.Def.Skin = Text() ?? ""; return unit; } List ReadUnitList(string body) { int count = body.Length > 0 ? int.Parse(body, Nfi) : 0; var list = new List(count); for (int i = 0; i < count; i++) list.Add(ReadUnit()); return list; } } } }