diff --git a/Console/BattleTech/BTConfig.xml b/Console/BattleTech/BTConfig.xml
new file mode 100644
index 0000000..9b547f0
--- /dev/null
+++ b/Console/BattleTech/BTConfig.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Console/TeslaConsole.BattleTech/BTCamera.cs b/Console/TeslaConsole.BattleTech/BTCamera.cs
new file mode 100644
index 0000000..1d9f91e
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTCamera.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Text;
+
+namespace TeslaConsole.BattleTech;
+
+///
+/// A spectator / mission-review station. Mirrors RPCamera: a non-combatant
+/// participant that occupies a pilot slot but is not a mech. No BT camera golden
+/// egg exists, so the field set follows the Red Planet camera block (minus the
+/// RP-only football team= field) and should be confirmed against a live BT
+/// pod when one is available.
+///
+[Serializable]
+internal class BTCamera : BTParticipant
+{
+ public BTCamera(string podHostAddress, HostType hostType)
+ : base(podHostAddress, hostType)
+ {
+ if (hostType != HostType.MissionReviewHostType)
+ {
+ throw new ArgumentOutOfRangeException("hostType");
+ }
+ }
+
+ protected sealed override void AppendParticipantSpecificData(int index, StringBuilder sb)
+ {
+ sb.Append("loadzones=0\n");
+ sb.Append("name=Camera\n");
+ sb.AppendFormat("bitmapindex={0}\n", index);
+ sb.Append("vehicle=camera\n");
+ sb.Append("badge=None\n");
+ sb.Append("dropzone=one\n");
+ sb.Append("color=Black\n");
+ }
+}
diff --git a/Console/TeslaConsole.BattleTech/BTConfig.cs b/Console/TeslaConsole.BattleTech/BTConfig.cs
new file mode 100644
index 0000000..50faa26
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTConfig.cs
@@ -0,0 +1,117 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Windows.Forms;
+using System.Xml;
+
+namespace TeslaConsole.BattleTech;
+
+///
+/// Loads and exposes the BattleTech data catalog (BattleTech\BTConfig.xml),
+/// mirroring TeslaConsole.RedPlanet.RPConfig. The single BT scenario is
+/// "Free For All"; No Return reuses the same catalog and differs only by role.
+///
+public static class BTConfig
+{
+ private static readonly BTScenario mFreeForAll;
+
+ private static readonly Dictionary mMaps;
+
+ private static readonly Dictionary mVehicles;
+
+ private static readonly Dictionary mTimesOfDay;
+
+ private static readonly Dictionary mWeather;
+
+ public static BTScenario FreeForAll => mFreeForAll;
+
+ public static Dictionary Maps => mMaps;
+
+ public static Dictionary Vehicles => mVehicles;
+
+ public static Dictionary TimesOfDay => mTimesOfDay;
+
+ public static Dictionary Weather => mWeather;
+
+ static BTConfig()
+ {
+ mMaps = new Dictionary();
+ mVehicles = new Dictionary();
+ mTimesOfDay = new Dictionary();
+ mWeather = new Dictionary();
+ XmlNode freeForAllNode = null;
+ XmlDocument xmlDocument = new XmlDocument();
+ xmlDocument.Load(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "BattleTech\\BTConfig.xml"));
+ foreach (XmlNode childNode in xmlDocument.DocumentElement.ChildNodes)
+ {
+ switch (childNode.Name)
+ {
+ case "freeforall":
+ freeForAllNode = childNode;
+ break;
+ case "map":
+ {
+ BTMap bTMap = new BTMap(childNode);
+ mMaps.Add(bTMap.Key, bTMap);
+ break;
+ }
+ case "vehicle":
+ {
+ BTVehicle bTVehicle = new BTVehicle(childNode);
+ mVehicles.Add(bTVehicle.Key, bTVehicle);
+ break;
+ }
+ case "time":
+ AddKeyNameNode(childNode, mTimesOfDay);
+ break;
+ case "weather":
+ {
+ BTWeather bTWeather = new BTWeather(childNode);
+ mWeather.Add(bTWeather.Key, bTWeather);
+ break;
+ }
+ }
+ }
+ mFreeForAll = new BTScenario(freeForAllNode);
+ }
+
+ internal static void AddKeyNameNode(XmlNode node, Dictionary list)
+ {
+ list.Add(node.Attributes["key"].InnerText, node.Attributes["name"].InnerText);
+ }
+
+ public static string MapNameOrKey(string mapKey)
+ {
+ if (!mMaps.ContainsKey(mapKey))
+ {
+ return mapKey;
+ }
+ return mMaps[mapKey].Name;
+ }
+
+ public static string VehicleNameOrKey(string vehicleKey)
+ {
+ if (!mVehicles.ContainsKey(vehicleKey))
+ {
+ return vehicleKey;
+ }
+ return mVehicles[vehicleKey].Name;
+ }
+
+ public static string TimeOfDayNameOrKey(string timeOfDayKey)
+ {
+ if (!mTimesOfDay.ContainsKey(timeOfDayKey))
+ {
+ return timeOfDayKey;
+ }
+ return mTimesOfDay[timeOfDayKey];
+ }
+
+ public static string WeatherNameOrKey(string weatherKey)
+ {
+ if (!mWeather.ContainsKey(weatherKey))
+ {
+ return weatherKey;
+ }
+ return mWeather[weatherKey].Name;
+ }
+}
diff --git a/Console/TeslaConsole.BattleTech/BTFreeForAllMission.cs b/Console/TeslaConsole.BattleTech/BTFreeForAllMission.cs
new file mode 100644
index 0000000..9f88981
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTFreeForAllMission.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace TeslaConsole.BattleTech;
+
+///
+/// The BattleTech battle mission. Both operator game modes — Free For All and
+/// No Return — are produced by this class and send scenario=freeforall on
+/// the wire (confirmed by both golden eggs); the two modes differ only in the
+/// role assigned to each .
+///
+[Serializable]
+internal class BTFreeForAllMission : BTMission
+{
+ public const string ScenarioTag = "freeforall";
+
+ private readonly List mPlayers = new List();
+
+ public List BattlePlayers => mPlayers;
+
+ public override IEnumerable Players => mPlayers.ToArray();
+
+ public override int PlayerCount => mPlayers.Count;
+
+ public BTFreeForAllMission(string mapKey, string timeOfDayKey, string weatherKey, int temperature, TimeSpan gameLength)
+ : base(ScenarioTag, mapKey, timeOfDayKey, weatherKey, temperature, gameLength)
+ {
+ }
+
+ protected override void AppendMissionSpecificData(StringBuilder egg)
+ {
+ }
+}
diff --git a/Console/TeslaConsole.BattleTech/BTMap.cs b/Console/TeslaConsole.BattleTech/BTMap.cs
new file mode 100644
index 0000000..fa1a2cf
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTMap.cs
@@ -0,0 +1,44 @@
+using System.Drawing;
+using System.IO;
+using System.Windows.Forms;
+using System.Xml;
+
+namespace TeslaConsole.BattleTech;
+
+public class BTMap
+{
+ private readonly string mKey;
+
+ private readonly string mName;
+
+ private readonly string mImagePath;
+
+ public string Key => mKey;
+
+ public string Name => mName;
+
+ public Image Image
+ {
+ get
+ {
+ if (mImagePath == null)
+ {
+ return null;
+ }
+ return ImageCache.GetImage(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), mImagePath));
+ }
+ }
+
+ internal BTMap(XmlNode mapNode)
+ {
+ XmlAttribute xmlAttribute = mapNode.Attributes["image"];
+ mKey = mapNode.Attributes["key"].InnerText;
+ mName = mapNode.Attributes["name"].InnerText;
+ mImagePath = xmlAttribute?.InnerText;
+ }
+
+ public override string ToString()
+ {
+ return mName;
+ }
+}
diff --git a/Console/TeslaConsole.BattleTech/BTMission.cs b/Console/TeslaConsole.BattleTech/BTMission.cs
new file mode 100644
index 0000000..a76d2d0
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTMission.cs
@@ -0,0 +1,323 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Munga.Net;
+
+namespace TeslaConsole.BattleTech;
+
+///
+/// A BattleTech mission and its egg serializer. Mirrors
+/// TeslaConsole.RedPlanet.RPMission: the wire framing
+/// () and the plasma bitmap / ordinal sections
+/// are identical to Red Planet. BattleTech differs only in the egg body — see
+/// (adventure name, non-zero temperature, no score
+/// compression, BT participant fields and the [Role::*] blocks).
+///
+[Serializable]
+internal abstract class BTMission
+{
+ private readonly string mScenarioKey;
+
+ private readonly string mMapKey;
+
+ private readonly string mTimeOfDayKey;
+
+ private readonly string mWeatherKey;
+
+ private readonly int mTemperature;
+
+ private readonly TimeSpan mGameLength;
+
+ private readonly List mCameras = new List();
+
+ public string ScenarioKey => mScenarioKey;
+
+ public string MapKey => mMapKey;
+
+ public string TimeOfDayKey => mTimeOfDayKey;
+
+ public string WeatherKey => mWeatherKey;
+
+ public int Temperature => mTemperature;
+
+ public TimeSpan GameLength => mGameLength;
+
+ public abstract IEnumerable Players { get; }
+
+ public abstract int PlayerCount { get; }
+
+ public List Cameras => mCameras;
+
+ protected BTMission(string scenarioKey, string mapKey, string timeOfDayKey, string weatherKey, int temperature, TimeSpan gameLength)
+ {
+ if (scenarioKey == null)
+ {
+ throw new ArgumentNullException("scenarioKey");
+ }
+ if (mapKey == null)
+ {
+ throw new ArgumentNullException("mapKey");
+ }
+ if (timeOfDayKey == null)
+ {
+ throw new ArgumentNullException("timeOfDayKey");
+ }
+ if (weatherKey == null)
+ {
+ throw new ArgumentNullException("weatherKey");
+ }
+ if (gameLength <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException("gameLength");
+ }
+ mScenarioKey = scenarioKey;
+ mMapKey = mapKey;
+ mTimeOfDayKey = timeOfDayKey;
+ mWeatherKey = weatherKey;
+ mTemperature = temperature;
+ mGameLength = gameLength;
+ }
+
+ public EggFileMessage[] ToEggFileMessages()
+ {
+ string text = ToEggString();
+ text = text.Replace("\r\n", "\0");
+ text = text.Replace('\n', '\0');
+ byte[] bytes = Encoding.ASCII.GetBytes(text);
+ EggFileMessage[] array = new EggFileMessage[(bytes.Length + 999) / 1000];
+ byte[] array2 = new byte[1000];
+ for (int i = 0; i < array.Length; i++)
+ {
+ int num = i * 1000;
+ int num2 = bytes.Length - num;
+ if (num2 > 1000)
+ {
+ num2 = 1000;
+ }
+ Buffer.BlockCopy(bytes, num, array2, 0, num2);
+ array[i] = new EggFileMessage(i, bytes.Length, num2, array2);
+ }
+ return array;
+ }
+
+ public string ToEggString()
+ {
+ StringBuilder stringBuilder = new StringBuilder();
+ List list = new List();
+ stringBuilder.Append("[mission]\n");
+ stringBuilder.Append("adventure=BattleTech\n");
+ stringBuilder.AppendFormat("map={0}\n", mMapKey);
+ stringBuilder.AppendFormat("scenario={0}\n", mScenarioKey);
+ stringBuilder.AppendFormat("time={0}\n", mTimeOfDayKey);
+ stringBuilder.AppendFormat("weather={0}\n", mWeatherKey);
+ stringBuilder.AppendFormat("temperature={0}\n", mTemperature);
+ stringBuilder.AppendFormat("length={0}\n", mGameLength.TotalSeconds);
+ stringBuilder.Append("[pilots]\n");
+ foreach (BTPlayer player in Players)
+ {
+ stringBuilder.AppendFormat("pilot={0}\n", player.PodHostAddress);
+ }
+ foreach (BTCamera camera in Cameras)
+ {
+ stringBuilder.AppendFormat("pilot={0}\n", camera.PodHostAddress);
+ }
+ int num = 1;
+ List roles = new List();
+ foreach (BTPlayer player2 in Players)
+ {
+ player2.AppendParticipant(num++, stringBuilder);
+ list.Add(player2.Name);
+ if (!roles.Exists((BTRole r) => r.Key == player2.Role.Key))
+ {
+ roles.Add(player2.Role);
+ }
+ }
+ foreach (BTCamera camera2 in Cameras)
+ {
+ camera2.AppendParticipant(num++, stringBuilder);
+ list.Add("Camera");
+ }
+ AppendMissionSpecificData(stringBuilder);
+ stringBuilder.Append("[largebitmap]\n");
+ foreach (string item in list)
+ {
+ stringBuilder.AppendFormat("bitmap=BitMap::Large::{0}\n", item);
+ }
+ stringBuilder.Append("[smallbitmap]\n");
+ foreach (string item2 in list)
+ {
+ stringBuilder.AppendFormat("bitmap=BitMap::Small::{0}\n", item2);
+ }
+ foreach (string item3 in list)
+ {
+ stringBuilder.AppendFormat("[BitMap::Large::{0}]\n", item3);
+ stringBuilder.Append(PlasmaBitmaps.GenerateString(128, 32, item3));
+ stringBuilder.Append("width=8\n");
+ stringBuilder.AppendFormat("[BitMap::Small::{0}]\n", item3);
+ stringBuilder.Append(PlasmaBitmaps.GenerateString(64, 16, item3));
+ stringBuilder.Append("width=4\n");
+ }
+ foreach (BTRole role in roles)
+ {
+ stringBuilder.AppendFormat("[Role::{0}]\n", role.Key);
+ stringBuilder.AppendFormat("model={0}\n", role.Model);
+ }
+ AppendOrdinals(stringBuilder);
+ return stringBuilder.ToString();
+ }
+
+ protected abstract void AppendMissionSpecificData(StringBuilder egg);
+
+ private static void AppendOrdinals(StringBuilder sb)
+ {
+ sb.Append("[ordinals]\n");
+ sb.Append("bitmap=Ordinal::BitMap::1\n");
+ sb.Append("bitmap=Ordinal::BitMap::2\n");
+ sb.Append("bitmap=Ordinal::BitMap::3\n");
+ sb.Append("bitmap=Ordinal::BitMap::4\n");
+ sb.Append("[Ordinal::BitMap::1]\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=000038000003C000001FF00000000F00\n");
+ sb.Append("bitmap=0000F8000003C000003FF80000000F00\n");
+ sb.Append("bitmap=0001F8000003C00000707C0000000F00\n");
+ sb.Append("bitmap=0001F8000003C00000603C0000000F00\n");
+ sb.Append("bitmap=00007801FC0FF00000003C3DF807FF00\n");
+ sb.Append("bitmap=00007803FE0FF00000003C3FFC0FFF00\n");
+ sb.Append("bitmap=00007803C703C00000003C3E3C1F0F00\n");
+ sb.Append("bitmap=000078078303C0000000783C1E1E0F00\n");
+ sb.Append("bitmap=000078078003C0000000783C1E1E0F00\n");
+ sb.Append("bitmap=00007807C003C0000000F03C1E1E0F00\n");
+ sb.Append("bitmap=00007807F003C0000001E03C1E1E0F00\n");
+ sb.Append("bitmap=00007803FC03C0000003C03C1E1E0F00\n");
+ sb.Append("bitmap=00007801FE03C0000007803C1E1E0F00\n");
+ sb.Append("bitmap=000078007F03C000000F003C1E1E0F00\n");
+ sb.Append("bitmap=000078001F03C000001E003C1E1E0F00\n");
+ sb.Append("bitmap=000078000F03C000003C003C1E1E0F00\n");
+ sb.Append("bitmap=000078060F03C0000078003C1E1E0F00\n");
+ sb.Append("bitmap=000078071E03E0000078003C1E1F1F00\n");
+ sb.Append("bitmap=00007803FE01F000007FFC3C1E0FFF00\n");
+ sb.Append("bitmap=00007801FC00F000007FFC3C1E07EF00\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("x=128\n");
+ sb.Append("y=32\n");
+ sb.Append("width=8\n");
+ sb.Append("[Ordinal::BitMap::2]\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=001FFF0000003C0000003C03C0780000\n");
+ sb.Append("bitmap=001FFF0000003C0000007C03C0780000\n");
+ sb.Append("bitmap=00000E0000003C000000FC03C0780000\n");
+ sb.Append("bitmap=00003C0000003C000001BC03C0780000\n");
+ sb.Append("bitmap=0000700F3E1FFC000003BC0FF07BF000\n");
+ sb.Append("bitmap=0001E00F7E3FFC0000073C0FF07FF800\n");
+ sb.Append("bitmap=0003800FFE7C3C0000063C03C07C7800\n");
+ sb.Append("bitmap=0007F80FFE783C00000C3C03C0783C00\n");
+ sb.Append("bitmap=0007FE0F80783C0000183C03C0783C00\n");
+ sb.Append("bitmap=00001E0F00783C0000383C03C0783C00\n");
+ sb.Append("bitmap=00000F0F00783C0000703C03C0783C00\n");
+ sb.Append("bitmap=00000F0F00783C00007FFF03C0783C00\n");
+ sb.Append("bitmap=00000F0F00783C00007FFF03C0783C00\n");
+ sb.Append("bitmap=00000F0F00783C0000003C03C0783C00\n");
+ sb.Append("bitmap=00000F0F00783C0000003C03C0783C00\n");
+ sb.Append("bitmap=00000F0F00783C0000003C03C0783C00\n");
+ sb.Append("bitmap=00180F0F00783C0000003C03C0783C00\n");
+ sb.Append("bitmap=001C1F0F007C7C0000003C03E0783C00\n");
+ sb.Append("bitmap=000FFE0F003FFC0000003C01F0783C00\n");
+ sb.Append("bitmap=0007FC0F001FBC0000003C00F0783C00\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("x=128\n");
+ sb.Append("y=32\n");
+ sb.Append("width=8\n");
+ sb.Append("[Ordinal::BitMap::3]\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=001FFF03C07800000000FC03C0780000\n");
+ sb.Append("bitmap=001FFF03C07800000003FC03C0780000\n");
+ sb.Append("bitmap=001E0003C07800000007C003C0780000\n");
+ sb.Append("bitmap=001E0003C0780000000F0003C0780000\n");
+ sb.Append("bitmap=001E000FF07BF000000F000FF07BF000\n");
+ sb.Append("bitmap=001E000FF07FF800001E000FF07FF800\n");
+ sb.Append("bitmap=001E0003C07C7800001E0003C07C7800\n");
+ sb.Append("bitmap=001FFC03C0783C00001EFC03C0783C00\n");
+ sb.Append("bitmap=001FFE03C0783C00001FFE03C0783C00\n");
+ sb.Append("bitmap=00001F03C0783C00001F1F03C0783C00\n");
+ sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=00000F03C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=00180F03C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=001C1F03E0783C00001F1F03E0783C00\n");
+ sb.Append("bitmap=000FFE01F0783C00000FFE01F0783C00\n");
+ sb.Append("bitmap=0007FC00F0783C000007FC00F0783C00\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("x=128\n");
+ sb.Append("y=32\n");
+ sb.Append("width=8\n");
+ sb.Append("[Ordinal::BitMap::4]\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=001FFF03C07800000007FC03C0780000\n");
+ sb.Append("bitmap=001FFF03C0780000000FFE03C0780000\n");
+ sb.Append("bitmap=00000F03C0780000001F1F03C0780000\n");
+ sb.Append("bitmap=00000F03C0780000001E0F03C0780000\n");
+ sb.Append("bitmap=00000F0FF07BF000001E0F0FF07BF000\n");
+ sb.Append("bitmap=00001F0FF07FF800001E0F0FF07FF800\n");
+ sb.Append("bitmap=00001E03C07C7800001E0F03C07C7800\n");
+ sb.Append("bitmap=00003E03C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=00003C03C0783C00000F1E03C0783C00\n");
+ sb.Append("bitmap=00003C03C0783C000007FC03C0783C00\n");
+ sb.Append("bitmap=00007803C0783C000007FC03C0783C00\n");
+ sb.Append("bitmap=00007803C0783C00000F1E03C0783C00\n");
+ sb.Append("bitmap=00007803C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=0000F003C0783C00001E0F03C0783C00\n");
+ sb.Append("bitmap=0000F003E0783C00000F1E03E0783C00\n");
+ sb.Append("bitmap=0000F001F0783C00000FFE01F0783C00\n");
+ sb.Append("bitmap=0000F000F0783C000007FC00F0783C00\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("bitmap=00000000000000000000000000000000\n");
+ sb.Append("x=128\n");
+ sb.Append("y=32\n");
+ sb.Append("width=8\n");
+ }
+}
diff --git a/Console/TeslaConsole.BattleTech/BTParticipant.cs b/Console/TeslaConsole.BattleTech/BTParticipant.cs
new file mode 100644
index 0000000..e622488
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTParticipant.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Text;
+
+namespace TeslaConsole.BattleTech;
+
+[Serializable]
+internal abstract class BTParticipant
+{
+ private readonly string mPodHostAddress;
+
+ private readonly HostType mHostType;
+
+ public string PodHostAddress => mPodHostAddress;
+
+ public HostType HostType => mHostType;
+
+ protected BTParticipant(string podHostAddress, HostType hostType)
+ {
+ mPodHostAddress = podHostAddress;
+ mHostType = hostType;
+ }
+
+ public void AppendParticipant(int index, StringBuilder sb)
+ {
+ sb.AppendFormat("[{0}]\n", mPodHostAddress);
+ sb.AppendFormat("hostType={0}\n", (int)mHostType);
+ AppendParticipantSpecificData(index, sb);
+ }
+
+ protected abstract void AppendParticipantSpecificData(int index, StringBuilder sb);
+}
diff --git a/Console/TeslaConsole.BattleTech/BTPlayer.cs b/Console/TeslaConsole.BattleTech/BTPlayer.cs
new file mode 100644
index 0000000..8affd98
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTPlayer.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Text;
+
+namespace TeslaConsole.BattleTech;
+
+///
+/// A BattleTech combatant (mech pilot). Mirrors RPPlayer but carries the
+/// BT-only participant fields (advanced damage model, pilot experience, mech
+/// value, faction emblem, camo, patch colour and role). Free For All and No
+/// Return use the same player type and differ only by the assigned
+/// .
+///
+[Serializable]
+internal class BTPlayer : BTParticipant
+{
+ private readonly string mName;
+
+ private readonly string mVehicleKey;
+
+ private readonly string mCamoKey;
+
+ private readonly string mPatchKey;
+
+ private readonly string mEmblemKey;
+
+ private readonly string mExperienceKey;
+
+ private readonly BTRole mRole;
+
+ private readonly bool mAdvancedDamage;
+
+ private readonly string mDropZoneKey;
+
+ private readonly int mVehicleValue;
+
+ public string Name => mName;
+
+ public string VehicleKey => mVehicleKey;
+
+ public string CamoKey => mCamoKey;
+
+ public string PatchKey => mPatchKey;
+
+ public string EmblemKey => mEmblemKey;
+
+ public string ExperienceKey => mExperienceKey;
+
+ public BTRole Role => mRole;
+
+ public bool AdvancedDamage => mAdvancedDamage;
+
+ public string DropZoneKey => mDropZoneKey;
+
+ public int VehicleValue => mVehicleValue;
+
+ public BTPlayer(string podHostAddress, string name, string vehicleKey, string camoKey, string patchKey, string emblemKey, string experienceKey, BTRole role, bool advancedDamage = true, string dropZoneKey = "one", int vehicleValue = 0)
+ : base(podHostAddress, HostType.GameMachineHostType)
+ {
+ if (podHostAddress == null)
+ {
+ throw new ArgumentNullException("podHostAddress");
+ }
+ if (name == null)
+ {
+ throw new ArgumentNullException("name");
+ }
+ if (vehicleKey == null)
+ {
+ throw new ArgumentNullException("vehicleKey");
+ }
+ if (camoKey == null)
+ {
+ throw new ArgumentNullException("camoKey");
+ }
+ if (patchKey == null)
+ {
+ throw new ArgumentNullException("patchKey");
+ }
+ if (emblemKey == null)
+ {
+ throw new ArgumentNullException("emblemKey");
+ }
+ if (experienceKey == null)
+ {
+ throw new ArgumentNullException("experienceKey");
+ }
+ if (role == null)
+ {
+ throw new ArgumentNullException("role");
+ }
+ if (dropZoneKey == null)
+ {
+ throw new ArgumentNullException("dropZoneKey");
+ }
+ mName = name;
+ mVehicleKey = vehicleKey;
+ mCamoKey = camoKey;
+ mPatchKey = patchKey;
+ mEmblemKey = emblemKey;
+ mExperienceKey = experienceKey;
+ mRole = role;
+ mAdvancedDamage = advancedDamage;
+ mDropZoneKey = dropZoneKey;
+ mVehicleValue = vehicleValue;
+ }
+
+ protected sealed override void AppendParticipantSpecificData(int index, StringBuilder sb)
+ {
+ sb.AppendFormat("advancedDamage={0}\n", mAdvancedDamage ? "1" : "0");
+ sb.Append("loadzones=1\n");
+ sb.AppendFormat("name={0}\n", mName);
+ sb.AppendFormat("bitmapindex={0}\n", index);
+ sb.AppendFormat("experience={0}\n", mExperienceKey);
+ sb.AppendFormat("vehicle={0}\n", mVehicleKey);
+ sb.AppendFormat("vehicleValue={0}\n", mVehicleValue);
+ sb.AppendFormat("badge={0}\n", mEmblemKey);
+ sb.AppendFormat("dropzone={0}\n", mDropZoneKey);
+ sb.AppendFormat("color={0}\n", mCamoKey);
+ sb.AppendFormat("patch={0}\n", mPatchKey);
+ sb.AppendFormat("role=Role::{0}\n", mRole.Key);
+ }
+}
diff --git a/Console/TeslaConsole.BattleTech/BTRole.cs b/Console/TeslaConsole.BattleTech/BTRole.cs
new file mode 100644
index 0000000..17ac67f
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTRole.cs
@@ -0,0 +1,44 @@
+using System.Xml;
+
+namespace TeslaConsole.BattleTech;
+
+///
+/// A BattleTech pilot role. Each participant references one role
+/// (role=Role::<Key>); the egg then carries a matching
+/// [Role::<Key>] model=<Model> block. Free For All uses the
+/// Default role, No Return uses the NoReturn role.
+///
+public class BTRole
+{
+ private readonly string mKey;
+
+ private readonly string mModel;
+
+ private readonly string mName;
+
+ public string Key => mKey;
+
+ public string Model => mModel;
+
+ public string Name => mName;
+
+ internal BTRole(XmlNode roleNode)
+ {
+ mKey = roleNode.Attributes["key"].InnerText;
+ mModel = roleNode.Attributes["model"].InnerText;
+ XmlAttribute xmlAttribute = roleNode.Attributes["name"];
+ mName = ((xmlAttribute != null) ? xmlAttribute.InnerText : mKey);
+ }
+
+ public BTRole(string key, string model, string name)
+ {
+ mKey = key;
+ mModel = model;
+ mName = name;
+ }
+
+ public override string ToString()
+ {
+ return mName;
+ }
+}
diff --git a/Console/TeslaConsole.BattleTech/BTScenario.cs b/Console/TeslaConsole.BattleTech/BTScenario.cs
new file mode 100644
index 0000000..9c26b89
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTScenario.cs
@@ -0,0 +1,132 @@
+using System.Collections.Generic;
+using System.Xml;
+
+namespace TeslaConsole.BattleTech;
+
+///
+/// A BattleTech scenario (game mode). Mirrors the Red Planet
+/// Scenario/DeathRaceScenario pattern: it inherits the shared
+/// map/vehicle/time/weather catalogs from (minus any
+/// <invalid> exclusions) and adds the BT-specific option lists
+/// (experience, camo, patch, emblem, role).
+///
+public class BTScenario
+{
+ private readonly string mKey;
+
+ private readonly string mName;
+
+ private readonly Dictionary mMaps = new Dictionary();
+
+ private readonly Dictionary mVehicles = new Dictionary();
+
+ private readonly Dictionary mTimesOfDay = new Dictionary();
+
+ private readonly Dictionary mWeather = new Dictionary();
+
+ private readonly Dictionary mExperiences = new Dictionary();
+
+ private readonly Dictionary mColors = new Dictionary();
+
+ private readonly Dictionary mPatches = new Dictionary();
+
+ private readonly Dictionary mBadges = new Dictionary();
+
+ private readonly Dictionary mRoles = new Dictionary();
+
+ public string Key => mKey;
+
+ public string Name => mName;
+
+ public Dictionary Maps => mMaps;
+
+ public Dictionary Vehicles => mVehicles;
+
+ public Dictionary TimesOfDay => mTimesOfDay;
+
+ public Dictionary Weather => mWeather;
+
+ public Dictionary Experiences => mExperiences;
+
+ public Dictionary Colors => mColors;
+
+ public Dictionary Patches => mPatches;
+
+ public Dictionary Badges => mBadges;
+
+ public Dictionary Roles => mRoles;
+
+ internal BTScenario(XmlNode scenarioNode)
+ {
+ mKey = scenarioNode.Name;
+ mName = scenarioNode.Attributes["name"].InnerText;
+ List> invalid = new List>();
+ foreach (XmlNode childNode in scenarioNode.ChildNodes)
+ {
+ switch (childNode.Name)
+ {
+ case "invalid":
+ invalid.Add(new KeyValuePair(childNode.Attributes["type"].InnerText, childNode.Attributes["key"].InnerText));
+ break;
+ case "experience":
+ BTConfig.AddKeyNameNode(childNode, mExperiences);
+ break;
+ case "color":
+ BTConfig.AddKeyNameNode(childNode, mColors);
+ break;
+ case "patch":
+ BTConfig.AddKeyNameNode(childNode, mPatches);
+ break;
+ case "badge":
+ BTConfig.AddKeyNameNode(childNode, mBadges);
+ break;
+ case "role":
+ {
+ BTRole role = new BTRole(childNode);
+ mRoles.Add(role.Key, role);
+ break;
+ }
+ }
+ }
+ foreach (BTMap value in BTConfig.Maps.Values)
+ {
+ if (!IsInvalid("map", value.Key, invalid))
+ {
+ mMaps.Add(value.Key, value);
+ }
+ }
+ foreach (BTVehicle value2 in BTConfig.Vehicles.Values)
+ {
+ if (!IsInvalid("vehicle", value2.Key, invalid))
+ {
+ mVehicles.Add(value2.Key, value2);
+ }
+ }
+ foreach (KeyValuePair item in BTConfig.TimesOfDay)
+ {
+ if (!IsInvalid("time", item.Key, invalid))
+ {
+ mTimesOfDay.Add(item.Key, item.Value);
+ }
+ }
+ foreach (BTWeather value3 in BTConfig.Weather.Values)
+ {
+ if (!IsInvalid("weather", value3.Key, invalid))
+ {
+ mWeather.Add(value3.Key, value3);
+ }
+ }
+ }
+
+ private static bool IsInvalid(string type, string value, List> invalid)
+ {
+ foreach (KeyValuePair item in invalid)
+ {
+ if (item.Key == type && item.Value == value)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/Console/TeslaConsole.BattleTech/BTVehicle.cs b/Console/TeslaConsole.BattleTech/BTVehicle.cs
new file mode 100644
index 0000000..fa145ec
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTVehicle.cs
@@ -0,0 +1,44 @@
+using System.Drawing;
+using System.IO;
+using System.Windows.Forms;
+using System.Xml;
+
+namespace TeslaConsole.BattleTech;
+
+public class BTVehicle
+{
+ private readonly string mKey;
+
+ private readonly string mName;
+
+ private readonly string mImagePath;
+
+ public string Key => mKey;
+
+ public string Name => mName;
+
+ public Image Image
+ {
+ get
+ {
+ if (mImagePath == null)
+ {
+ return null;
+ }
+ return ImageCache.GetImage(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), mImagePath));
+ }
+ }
+
+ internal BTVehicle(XmlNode vehicleNode)
+ {
+ XmlAttribute xmlAttribute = vehicleNode.Attributes["image"];
+ mKey = vehicleNode.Attributes["key"].InnerText;
+ mName = vehicleNode.Attributes["name"].InnerText;
+ mImagePath = xmlAttribute?.InnerText;
+ }
+
+ public override string ToString()
+ {
+ return mName;
+ }
+}
diff --git a/Console/TeslaConsole.BattleTech/BTWeather.cs b/Console/TeslaConsole.BattleTech/BTWeather.cs
new file mode 100644
index 0000000..fed971f
--- /dev/null
+++ b/Console/TeslaConsole.BattleTech/BTWeather.cs
@@ -0,0 +1,36 @@
+using System.Xml;
+
+namespace TeslaConsole.BattleTech;
+
+///
+/// A BattleTech weather option. Unlike Red Planet (where weather is a plain
+/// key/name pair), a BT weather entry also carries the ambient temperature that
+/// becomes the egg's [mission] temperature= field.
+///
+public class BTWeather
+{
+ private readonly string mKey;
+
+ private readonly string mName;
+
+ private readonly int mTemperature;
+
+ public string Key => mKey;
+
+ public string Name => mName;
+
+ public int Temperature => mTemperature;
+
+ internal BTWeather(XmlNode weatherNode)
+ {
+ mKey = weatherNode.Attributes["key"].InnerText;
+ mName = weatherNode.Attributes["name"].InnerText;
+ XmlAttribute xmlAttribute = weatherNode.Attributes["temperature"];
+ mTemperature = ((xmlAttribute != null) ? int.Parse(xmlAttribute.InnerText) : 0);
+ }
+
+ public override string ToString()
+ {
+ return mName;
+ }
+}
diff --git a/Console/TeslaConsole.csproj b/Console/TeslaConsole.csproj
index 9529057..e397835 100644
--- a/Console/TeslaConsole.csproj
+++ b/Console/TeslaConsole.csproj
@@ -30,6 +30,7 @@
+
diff --git a/Console/tests/TeslaConsole.DiffTests/BTGoldenEggTests.cs b/Console/tests/TeslaConsole.DiffTests/BTGoldenEggTests.cs
new file mode 100644
index 0000000..886d6f9
--- /dev/null
+++ b/Console/tests/TeslaConsole.DiffTests/BTGoldenEggTests.cs
@@ -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
+{
+ ///
+ /// 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.
+ 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);
+ }
+ }
+}
diff --git a/Console/tests/TeslaConsole.DiffTests/BattleTech/TESTARN.EGG b/Console/tests/TeslaConsole.DiffTests/BattleTech/TESTARN.EGG
new file mode 100644
index 0000000..9acf001
--- /dev/null
+++ b/Console/tests/TeslaConsole.DiffTests/BattleTech/TESTARN.EGG
@@ -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
diff --git a/Console/tests/TeslaConsole.DiffTests/BattleTech/cavern.egg b/Console/tests/TeslaConsole.DiffTests/BattleTech/cavern.egg
new file mode 100644
index 0000000..7e62762
Binary files /dev/null and b/Console/tests/TeslaConsole.DiffTests/BattleTech/cavern.egg differ
diff --git a/Console/tests/TeslaConsole.DiffTests/Invoker.cs b/Console/tests/TeslaConsole.DiffTests/Invoker.cs
index d789aaa..2420b80 100644
--- a/Console/tests/TeslaConsole.DiffTests/Invoker.cs
+++ b/Console/tests/TeslaConsole.DiffTests/Invoker.cs
@@ -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}";
}
+ ///
+ /// 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.
+ ///
+ 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);
+ }
+
+ ///
+ /// 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".
+ ///
+ 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}";
+ }
+
///
/// Deterministic black/white bitmap whose pixels depend only on (x, y, seed),
/// so both assemblies receive identical input pixels to pack.
diff --git a/Console/tests/TeslaConsole.DiffTests/README.md b/Console/tests/TeslaConsole.DiffTests/README.md
index bb4ce31..19e9118 100644
--- a/Console/tests/TeslaConsole.DiffTests/README.md
+++ b/Console/tests/TeslaConsole.DiffTests/README.md
@@ -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)
diff --git a/Console/tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj b/Console/tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj
index 052a2fb..b32b04a 100644
--- a/Console/tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj
+++ b/Console/tests/TeslaConsole.DiffTests/TeslaConsole.DiffTests.csproj
@@ -31,6 +31,11 @@
+
+
+
+
+