Files
TeslaSuite/Console/tests/TeslaConsole.DiffTests/BTGoldenEggTests.cs
T
CydandClaude Fable 5 18d787bdd5 BattleTech results printing, defaults dialog, and BT411 catalog (BT port phase 4)
Completes the BattleTech console feature to parity with Red Planet:

- BTPrintDocument: the per-pilot landscape score sheet (mech portrait,
  placing, kill/death/damage stats, randomized mission-highlights narrative,
  pilot-vs-pilot damage matrix, place-over-time chart), mirroring
  RPPrintDocument minus the football/lap/boost/score-zone concepts. Wired into
  BTGame (Auto Print checkbox + Print Last Mission button) and the shell's File
  menu (BT Mission Print Preview / Print BT Mission, loading .btm files).
- BTStrings(.xml): the BT mission-highlight narrative pools (damage tiers,
  kill, death-without-honor, recap), loaded from BattleTech\BTStrings.xml like
  RPStrings.
- BTDefaultsDialog: two-column (Free For All / No Return) editor for the BT
  operator defaults, reachable from Settings (Change/Import/Export BattleTech
  Defaults). Verified via UI Automation: opens with all 18 option combos
  populated.
- Apps.xml: the BattleTech 4.11 product (btl4.exe on the shared RP411 engine,
  same -net/-res/-lc/-mr command line; deploys to C:\Games\BT411) with Game
  Client / Live Camera / Mission Review launch entries. CatalogTests updated to
  4 products / 8 entries plus the three new BT entry assertions.

Role model spelling corrected to "noreturn": the Mac Console.ini tag and the
game's BTL4.RES role resource are dfltrole/NoReturn; the 4.10 console's eggs
emitted a broken "noretun" that cannot resolve against the RES. BTGoldenEggTests
and BTConfig.xml updated accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 23:04:03 -05:00

251 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 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.
/// </summary>
public class BTGoldenEggTests : IClassFixture<DifferentialFixture>
{
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:<Type>" 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<string, List<string>> ParseEgg(string text)
{
var sections = new Dictionary<string, List<string>>();
List<string> 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<string>();
sections.Add(name, current);
}
else
{
Assert.NotNull(current); // a key=value before any section header
current.Add(line);
}
}
return sections;
}
/// <summary>Sections whose bitmap= lines are pixel rows, not references.</summary>
private static bool IsPixelSection(string name)
=> name.StartsWith("BitMap::", StringComparison.Ordinal) ||
name.StartsWith("Ordinal::BitMap::", StringComparison.Ordinal);
private static Dictionary<string, List<string>> DropPixelRows(Dictionary<string, List<string>> 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 1st4th 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);
}
}
}