BattleTech egg builder, catalog, and golden-egg DiffTests (BT port phases 1-2)

New TeslaConsole.BattleTech namespace mirroring TeslaConsole.RedPlanet's
mission layer: BTMission/BTFreeForAllMission (egg serializer; RP wire framing
and ordinal bitmaps reused verbatim), BTParticipant/BTPlayer/BTCamera (BT
participant fields: advancedDamage, experience, vehicleValue, patch, role),
and the BTConfig/BTScenario/BTMap/BTVehicle/BTWeather/BTRole catalog loaded
from BattleTech\BTConfig.xml (reconstructed from the Mac Console 4.10 ini BT
tree). Free For All and No Return share one mission class - both send
scenario=freeforall; the mode is the role assigned to each pilot.

BTGoldenEggTests (7 tests, recovered-only) diff the generated egg
field-by-field against two eggs captured from the original consoles
(cavern.egg, TESTARN.EGG): order-independent per-section compare with
font-rendered bitmap pixel rows excluded, ordinal art byte-exact vs
TESTARN.EGG, EggFileMessage framing reassembly, role de-dup, No Return role
semantics, and shipped-catalog contents. Invoker gains BTEggString/
BTEggFraming reflection cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-07 22:28:07 -05:00
co-authored by Claude Fable 5
parent 0431c6840b
commit 9148ce2cca
19 changed files with 1668 additions and 1 deletions
@@ -0,0 +1,247 @@
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.
var args = (string[])CavernArgs.Clone();
args[13] = "NoReturn";
args[14] = "noretun";
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=noretun", 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("noretun", ffa.SelectSingleNode("role[@key='NoReturn']").Attributes["model"].InnerText);
}
}
}
@@ -0,0 +1,229 @@
[mission]
adventure=BattleTech
map=arena1
scenario=freeforall
time=day
weather=clear
temperature=27
length=600
[ordinals]
bitmap=Ordinal::BitMap::1
bitmap=Ordinal::BitMap::2
bitmap=Ordinal::BitMap::3
bitmap=Ordinal::BitMap::4
[Ordinal::BitMap::1]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000038000003C000001FF00000000F00
bitmap=0000F8000003C000003FF80000000F00
bitmap=0001F8000003C00000707C0000000F00
bitmap=0001F8000003C00000603C0000000F00
bitmap=00007801FC0FF00000003C3DF807FF00
bitmap=00007803FE0FF00000003C3FFC0FFF00
bitmap=00007803C703C00000003C3E3C1F0F00
bitmap=000078078303C0000000783C1E1E0F00
bitmap=000078078003C0000000783C1E1E0F00
bitmap=00007807C003C0000000F03C1E1E0F00
bitmap=00007807F003C0000001E03C1E1E0F00
bitmap=00007803FC03C0000003C03C1E1E0F00
bitmap=00007801FE03C0000007803C1E1E0F00
bitmap=000078007F03C000000F003C1E1E0F00
bitmap=000078001F03C000001E003C1E1E0F00
bitmap=000078000F03C000003C003C1E1E0F00
bitmap=000078060F03C0000078003C1E1E0F00
bitmap=000078071E03E0000078003C1E1F1F00
bitmap=00007803FE01F000007FFC3C1E0FFF00
bitmap=00007801FC00F000007FFC3C1E07EF00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::2]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF0000003C0000003C03C0780000
bitmap=001FFF0000003C0000007C03C0780000
bitmap=00000E0000003C000000FC03C0780000
bitmap=00003C0000003C000001BC03C0780000
bitmap=0000700F3E1FFC000003BC0FF07BF000
bitmap=0001E00F7E3FFC0000073C0FF07FF800
bitmap=0003800FFE7C3C0000063C03C07C7800
bitmap=0007F80FFE783C00000C3C03C0783C00
bitmap=0007FE0F80783C0000183C03C0783C00
bitmap=00001E0F00783C0000383C03C0783C00
bitmap=00000F0F00783C0000703C03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C00007FFF03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00000F0F00783C0000003C03C0783C00
bitmap=00180F0F00783C0000003C03C0783C00
bitmap=001C1F0F007C7C0000003C03E0783C00
bitmap=000FFE0F003FFC0000003C01F0783C00
bitmap=0007FC0F001FBC0000003C00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::3]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000000FC03C0780000
bitmap=001FFF03C07800000003FC03C0780000
bitmap=001E0003C07800000007C003C0780000
bitmap=001E0003C0780000000F0003C0780000
bitmap=001E000FF07BF000000F000FF07BF000
bitmap=001E000FF07FF800001E000FF07FF800
bitmap=001E0003C07C7800001E0003C07C7800
bitmap=001FFC03C0783C00001EFC03C0783C00
bitmap=001FFE03C0783C00001FFE03C0783C00
bitmap=00001F03C0783C00001F1F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00000F03C0783C00001E0F03C0783C00
bitmap=00180F03C0783C00001E0F03C0783C00
bitmap=001C1F03E0783C00001F1F03E0783C00
bitmap=000FFE01F0783C00000FFE01F0783C00
bitmap=0007FC00F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[Ordinal::BitMap::4]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=001FFF03C07800000007FC03C0780000
bitmap=001FFF03C0780000000FFE03C0780000
bitmap=00000F03C0780000001F1F03C0780000
bitmap=00000F03C0780000001E0F03C0780000
bitmap=00000F0FF07BF000001E0F0FF07BF000
bitmap=00001F0FF07FF800001E0F0FF07FF800
bitmap=00001E03C07C7800001E0F03C07C7800
bitmap=00003E03C0783C00001E0F03C0783C00
bitmap=00003C03C0783C00000F1E03C0783C00
bitmap=00003C03C0783C000007FC03C0783C00
bitmap=00007803C0783C000007FC03C0783C00
bitmap=00007803C0783C00000F1E03C0783C00
bitmap=00007803C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003C0783C00001E0F03C0783C00
bitmap=0000F003E0783C00000F1E03E0783C00
bitmap=0000F001F0783C00000FFE01F0783C00
bitmap=0000F000F0783C000007FC00F0783C00
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[pilots]
pilot=200.0.0.113
[200.0.0.113]
hostType=0
advancedDamage=1
loadzones=1
name=cyd
bitmapindex=1
experience=veteran
badge=VGL
patch=Yellow
role=Role::Default
dropzone=one
vehicle=madcat
vehicleValue=0
color=White
[largebitmap]
bitmap=BitMap::Large::cyd
[BitMap::Large::cyd]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000000000003C0000000000
bitmap=000000000000FCF00E0FBC0000000000
bitmap=000000000003FE781C1FFC0000000000
bitmap=000000000007FE781C3C7C0000000000
bitmap=00000000000F82783C3C3C0000000000
bitmap=00000000001F003C38783C0000000000
bitmap=00000000001E003C78783C0000000000
bitmap=00000000001E003C70783C0000000000
bitmap=00000000001E003E70783C0000000000
bitmap=00000000001E001EE0783C0000000000
bitmap=00000000001E001EE0783C0000000000
bitmap=00000000001F001FE0783C0000000000
bitmap=00000000000F020FC07C7C0000000000
bitmap=000000000007FE0FC03FFC0000000000
bitmap=000000000003FE0F803FFC0000000000
bitmap=000000000000FC07800F3C0000000000
bitmap=00000000000000070000000000000000
bitmap=00000000000000070000000000000000
bitmap=000000000000000F0000000000000000
bitmap=000000000000000E0000000000000000
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000000000
x=128
y=32
width=8
[smallbitmap]
bitmap=BitMap::Small::cyd
[BitMap::Small::cyd]
bitmap=00000000000000000000000000000000
bitmap=00000000000000000000000000600000
bitmap=00000000006000000000000000600000
bitmap=000001EC33E000000000030666600000
bitmap=000003066660000000000303C6600000
bitmap=00000303C66000000000030186600000
bitmap=000001E183E000000000000300000000
bitmap=00000003000000000000000000000000
x=64
y=16
width=4
[Role::Default]
model=dfltrole
[Role::NoReturn]
model=noretun
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Xml;
namespace TeslaConsole.DiffTests
@@ -194,6 +195,12 @@ namespace TeslaConsole.DiffTests
case "CatalogEntry":
return CatalogEntry(args[0], args[1], args[2], args[3]);
case "BTEggString":
return BTEggString(args);
case "BTEggFraming":
return BTEggFraming(args);
default:
throw new ArgumentException("Unknown case: " + caseName);
}
@@ -345,6 +352,88 @@ namespace TeslaConsole.DiffTests
return $"{disp}|{lkey:D}|{exe}|{margs}|{wd}|{ar}";
}
/// <summary>
/// Builds a TeslaConsole.BattleTech.BTFreeForAllMission from flat args:
/// [0]=mapKey [1]=timeOfDayKey [2]=weatherKey [3]=temperature [4]=lengthSeconds,
/// then one pilot per 10 args: podHost, name, vehicle, camo, patch, emblem,
/// experience, dropzone, roleKey, roleModel. The BT types are internal to the
/// exe, so construction goes through reflection like every other case.
/// </summary>
private object BuildBTMission(string[] args)
{
var missionType = T("TeslaConsole.BattleTech.BTFreeForAllMission");
object mission = Activator.CreateInstance(missionType, new object[]
{
args[0], args[1], args[2],
int.Parse(args[3], CultureInfo.InvariantCulture),
TimeSpan.FromSeconds(double.Parse(args[4], CultureInfo.InvariantCulture)),
});
var roleType = T("TeslaConsole.BattleTech.BTRole");
var playerType = T("TeslaConsole.BattleTech.BTPlayer");
object players = missionType.GetProperty("BattlePlayers").GetValue(mission, null);
MethodInfo add = players.GetType().GetMethod("Add");
for (int i = 5; i < args.Length; i += 10)
{
object role = Activator.CreateInstance(roleType,
new object[] { args[i + 8], args[i + 9], args[i + 8] });
object player = Activator.CreateInstance(playerType, new object[]
{
args[i], args[i + 1], args[i + 2], args[i + 3], args[i + 4],
args[i + 5], args[i + 6], role,
/* advancedDamage */ true, /* dropZoneKey */ args[i + 7],
/* vehicleValue */ 0,
});
add.Invoke(players, new[] { player });
}
return mission;
}
private string BTEggString(string[] args)
{
object mission = BuildBTMission(args);
return (string)mission.GetType().GetMethod("ToEggString").Invoke(mission, null);
}
/// <summary>
/// Exercises BTMission.ToEggFileMessages and verifies the RP-inherited wire
/// framing: newlines become NULs, ASCII bytes go out in 1000-byte chunks with
/// contiguous sequence numbers, and the chunks reassemble byte-exactly to the
/// ToEggString content. Returns "messages=N;total=L;framing=B;reassembled=B".
/// </summary>
private string BTEggFraming(string[] args)
{
object mission = BuildBTMission(args);
Type mt = mission.GetType();
string egg = (string)mt.GetMethod("ToEggString").Invoke(mission, null);
byte[] expected = Encoding.ASCII.GetBytes(
egg.Replace("\r\n", "\0").Replace('\n', '\0'));
var messages = (Array)mt.GetMethod("ToEggFileMessages").Invoke(mission, null);
if (messages.Length == 0)
return "no-messages";
Type et = messages.GetValue(0).GetType();
FieldInfo seq = et.GetField("SequenceNumber");
FieldInfo total = et.GetField("NotationFileLength");
FieldInfo len = et.GetField("ThisMessageLength");
FieldInfo data = et.GetField("NotationData");
bool framing = true;
var reassembled = new MemoryStream();
for (int i = 0; i < messages.Length; i++)
{
object m = messages.GetValue(i);
int chunk = (int)len.GetValue(m);
framing &= (int)seq.GetValue(m) == i;
framing &= (int)total.GetValue(m) == expected.Length;
framing &= chunk == (i < messages.Length - 1 ? 1000 : expected.Length - 1000 * i);
reassembled.Write((byte[])data.GetValue(m), 0, chunk);
}
bool bytesMatch = reassembled.ToArray().SequenceEqual(expected);
return $"messages={messages.Length};total={expected.Length};framing={framing};reassembled={bytesMatch}";
}
/// <summary>
/// Deterministic black/white bitmap whose pixels depend only on (x, y, seed),
/// so both assemblies receive identical input pixels to pack.
+19 -1
View File
@@ -27,7 +27,25 @@ proprietary dependencies — still resolves its references for metadata inspecti
and property/event accessor methods are excluded — the README at the repo root
notes those legitimately differ between a decompilation and the lost sources.
2. **Behavioral output** (`BehavioralEquivalenceTests`)
2. **Recovered-only characterization** (`CatalogTests`, `BTGoldenEggTests`)
Features that were *added* in the reconstruction have no counterpart in the
original exe, so these run against the recovered build only:
- `CatalogTests` — the data-driven product catalog reproduces the exact
`LaunchData` the old hardcoded code emitted.
- `BTGoldenEggTests` — the new `TeslaConsole.BattleTech` mission builder is
diffed field-by-field against two golden eggs captured from the original
consoles ([`BattleTech/cavern.egg`](BattleTech/cavern.egg),
[`BattleTech/TESTARN.EGG`](BattleTech/TESTARN.EGG)). The comparison is
per-section and order-independent (the pod parses eggs INI-style; the two
golden eggs themselves disagree on field order). Font-rendered name-bitmap
pixel rows are excluded, but TESTARN's ordinal art — identical to the
RP-inherited rows — is compared byte-exactly. Also covers the
`EggFileMessage` wire framing (NUL-delimited ASCII, 1000-byte chunks,
byte-exact reassembly), role-block de-duplication, the No Return mode
(same `scenario=freeforall`, different role), and the shipped
`BattleTech\BTConfig.xml` catalog contents.
3. **Behavioral output** (`BehavioralEquivalenceTests`)
The same deterministic, dependency-free methods are invoked in *both* assemblies
over a battery of inputs and the results must match byte-for-byte:
- `RPStrings.GetTimeString` (mm:ss formatting + 0.5 s rounding)
@@ -31,6 +31,11 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<!-- BattleTech golden eggs captured from the original consoles (see BTGoldenEggTests). -->
<Content Include="BattleTech\*.egg" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<!--
Build the reconstruction before the tests so we always compare against a fresh