using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Xml; using Xunit; namespace TeslaConsole.DiffTests { /// /// Golden-egg tests for the new TeslaConsole.BattleTech mission builder. BattleTech /// is absent from the original exe, so like CatalogTests these run against the /// recovered build only; the baselines are two eggs captured from the original /// consoles (BattleTech\cavern.egg, BattleTech\TESTARN.EGG — see the port spec in /// 410console/battletech-port/). /// /// The comparison is per-section and order-independent: the pod parses the egg /// INI-style, and the two golden eggs themselves disagree with each other on /// participant field order. Pixel rows of the font-rendered name bitmaps are /// excluded — the original consoles rendered them with a different font, and /// cavern.egg's ordinal art is Mac-rendered too (the spec's known false /// byte-match). TESTARN.EGG's ordinal art, however, is identical to the /// RP-inherited rows, so that one IS compared byte-exactly. /// public class BTGoldenEggTests : IClassFixture { private readonly DifferentialFixture _fx; public BTGoldenEggTests(DifferentialFixture fx) => _fx = fx; // Mission args: mapKey, timeOfDayKey, weatherKey, temperature, lengthSeconds, // then per pilot: podHost, name, vehicle, camo, patch, emblem, experience, // dropzone, roleKey, roleModel (matches Invoker.BuildBTMission). private static readonly string[] CavernArgs = { "cavern", "night", "clear", "27", "120", "200.0.0.113", "cyd", "madcat", "Grey", "Red", "VGL", "veteran", "one", "Default", "dfltrole", }; private static readonly string[] TestarnArgs = { "arena1", "day", "clear", "27", "600", "200.0.0.113", "cyd", "madcat", "White", "Yellow", "VGL", "veteran", "one", "Default", "dfltrole", }; private string Generate(string[] args) { string egg = _fx.Recovered.Run("BTEggString", args); Assert.StartsWith("[mission]", egg); // not an "EXC:" result return egg; } private static string ReadGolden(string fileName) { string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "BattleTech", fileName); Assert.True(File.Exists(path), "Golden egg not found: " + path); // On disk the egg is the wire form: NUL-delimited ASCII. return Encoding.ASCII.GetString(File.ReadAllBytes(path)); } // ---- egg parsing ---- private static Dictionary> ParseEgg(string text) { var sections = new Dictionary>(); List current = null; foreach (var raw in text.Split(new[] { '\0', '\n' }, StringSplitOptions.RemoveEmptyEntries)) { // TESTARN.EGG was captured with a stray \r before each NUL delimiter. var line = raw.TrimEnd('\r'); if (line.Length == 0) continue; if (line.StartsWith("[", StringComparison.Ordinal)) { string name = line.Trim('[', ']'); Assert.False(sections.ContainsKey(name), "Duplicate egg section: " + name); current = new List(); sections.Add(name, current); } else { Assert.NotNull(current); // a key=value before any section header current.Add(line); } } return sections; } /// Sections whose bitmap= lines are pixel rows, not references. private static bool IsPixelSection(string name) => name.StartsWith("BitMap::", StringComparison.Ordinal) || name.StartsWith("Ordinal::BitMap::", StringComparison.Ordinal); private static Dictionary> DropPixelRows(Dictionary> egg) => egg.ToDictionary( s => s.Key, s => IsPixelSection(s.Key) ? s.Value.Where(l => !l.StartsWith("bitmap=", StringComparison.Ordinal)).ToList() : s.Value); private static void AssertFieldsMatchGolden(string generated, string golden, params string[] acceptedExtraGoldenSections) { var gen = DropPixelRows(ParseEgg(generated)); var gold = DropPixelRows(ParseEgg(golden)); var missing = gold.Keys.Except(gen.Keys).Except(acceptedExtraGoldenSections) .OrderBy(s => s, StringComparer.Ordinal).ToArray(); var extra = gen.Keys.Except(gold.Keys) .OrderBy(s => s, StringComparer.Ordinal).ToArray(); Assert.True(missing.Length == 0, "Golden egg sections missing from the generated egg: " + string.Join(", ", missing)); Assert.True(extra.Length == 0, "Generated egg sections absent from the golden egg: " + string.Join(", ", extra)); foreach (var name in gen.Keys) { var g = gen[name].OrderBy(s => s, StringComparer.Ordinal).ToArray(); var r = gold[name].OrderBy(s => s, StringComparer.Ordinal).ToArray(); Assert.True(g.SequenceEqual(r), "[" + name + "] fields differ.\n golden: " + string.Join(" | ", r) + "\n generated: " + string.Join(" | ", g)); } } // ---- golden-egg field equivalence ---- [Fact] public void Cavern_Egg_Fields_Match_Golden() => AssertFieldsMatchGolden(Generate(CavernArgs), ReadGolden("cavern.egg")); [Fact] public void Testarn_Egg_Fields_Match_Golden() { // The original console emitted a [Role::*] block for every role in its // catalog; ours emits only the roles the pilots reference, so TESTARN's // unreferenced [Role::NoReturn] is an accepted difference (extra blocks // are harmless to the pod's INI-style parser; missing referenced ones // would not be). AssertFieldsMatchGolden(Generate(TestarnArgs), ReadGolden("TESTARN.EGG"), "Role::NoReturn"); } [Fact] public void Ordinal_Bitmap_Art_Matches_Testarn_Byte_Exactly() { // TESTARN.EGG's 1st–4th place art is identical to the RP ordinals the BT // builder reuses, so here the pixel rows are compared exactly and in order. // (cavern.egg is excluded on purpose: its ordinal art is Mac-font-rendered // and legitimately differs — the spec's known false byte-match.) var gen = ParseEgg(Generate(TestarnArgs)); var gold = ParseEgg(ReadGolden("TESTARN.EGG")); for (int n = 1; n <= 4; n++) { string name = "Ordinal::BitMap::" + n; Assert.True(gen.ContainsKey(name), "Generated egg lacks [" + name + "]"); Assert.Equal(gold[name], gen[name]); } } // ---- wire framing ---- [Fact] public void Egg_Wire_Framing_Chunks_And_Reassembles() { string result = _fx.Recovered.Run("BTEggFraming", CavernArgs); var m = Regex.Match(result, @"^messages=(\d+);total=(\d+);framing=True;reassembled=True$"); Assert.True(m.Success, "Unexpected framing result: " + result); int messages = int.Parse(m.Groups[1].Value); int total = int.Parse(m.Groups[2].Value); Assert.Equal((total + 999) / 1000, messages); Assert.True(total > 5000, "Suspiciously small egg (" + total + " bytes)"); } // ---- behavior pinned by the resolved spec questions ---- [Fact] public void Role_Blocks_Are_Deduplicated_Across_Pilots() { var args = CavernArgs.Concat(new[] { "200.0.0.114", "kai", "loki", "Black", "Blue", "Davion", "novice", "two", "Default", "dfltrole", }).ToArray(); string egg = Generate(args); int roleRefs = Regex.Matches(egg, Regex.Escape("role=Role::Default\n")).Count; int roleBlocks = Regex.Matches(egg, Regex.Escape("[Role::Default]\n")).Count; Assert.Equal(2, roleRefs); Assert.Equal(1, roleBlocks); } [Fact] public void NoReturn_Mode_Sends_Freeforall_Scenario_With_NoReturn_Role() { // No Return is not a separate scenario on the wire: both golden eggs say // scenario=freeforall, and the modes differ only by the pilots' role. // The model is "noreturn" (the Mac ini tag, resolvable against the // game's BTL4.RES "NoReturn" resource) — NOT the golden eggs' "noretun", // which was a 4.10 console bug that dropped a character. var args = (string[])CavernArgs.Clone(); args[13] = "NoReturn"; args[14] = "noreturn"; var egg = ParseEgg(Generate(args)); Assert.Contains("scenario=freeforall", egg["mission"]); Assert.Contains("role=Role::NoReturn", egg["200.0.0.113"]); Assert.True(egg.ContainsKey("Role::NoReturn"), "Missing [Role::NoReturn] block"); Assert.Contains("model=noreturn", egg["Role::NoReturn"]); Assert.False(egg.ContainsKey("Role::Default"), "Unreferenced [Role::Default] emitted"); } // ---- the shipped catalog ---- [Fact] public void BTConfig_Catalog_Ships_With_Build_And_Has_Expected_Counts() { // Verifies the csproj Content include actually lands BTConfig.xml next to // the exe (BTConfig loads it from there at runtime) and that the catalog // carries the full Mac Console.ini BT tree. string path = Path.Combine(Path.GetDirectoryName(AssemblyPaths.RecoveredExe), "BattleTech", "BTConfig.xml"); Assert.True(File.Exists(path), "BTConfig.xml not copied next to the build: " + path); var doc = new XmlDocument(); doc.Load(path); XmlElement root = doc.DocumentElement; Assert.Equal("BattleTech", root.Name); Assert.Equal(8, root.SelectNodes("map").Count); Assert.Equal(18, root.SelectNodes("vehicle").Count); Assert.Equal(4, root.SelectNodes("time").Count); Assert.Equal(3, root.SelectNodes("weather").Count); foreach (XmlNode weather in root.SelectNodes("weather")) Assert.Equal("27", weather.Attributes["temperature"].InnerText); XmlNode ffa = root.SelectSingleNode("freeforall"); Assert.NotNull(ffa); Assert.Equal(3, ffa.SelectNodes("experience").Count); Assert.Equal(7, ffa.SelectNodes("color").Count); Assert.Equal(8, ffa.SelectNodes("patch").Count); Assert.Equal(7, ffa.SelectNodes("badge").Count); Assert.Equal(2, ffa.SelectNodes("role").Count); Assert.Equal("dfltrole", ffa.SelectSingleNode("role[@key='Default']").Attributes["model"].InnerText); Assert.Equal("noreturn", ffa.SelectSingleNode("role[@key='NoReturn']").Attributes["model"].InnerText); } } }