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 @@ + + + + +