BattleTech egg builder, catalog, and golden-egg DiffTests (BT port phases 1-2)
New TeslaConsole.BattleTech namespace mirroring TeslaConsole.RedPlanet's mission layer: BTMission/BTFreeForAllMission (egg serializer; RP wire framing and ordinal bitmaps reused verbatim), BTParticipant/BTPlayer/BTCamera (BT participant fields: advancedDamage, experience, vehicleValue, patch, role), and the BTConfig/BTScenario/BTMap/BTVehicle/BTWeather/BTRole catalog loaded from BattleTech\BTConfig.xml (reconstructed from the Mac Console 4.10 ini BT tree). Free For All and No Return share one mission class - both send scenario=freeforall; the mode is the role assigned to each pilot. BTGoldenEggTests (7 tests, recovered-only) diff the generated egg field-by-field against two eggs captured from the original consoles (cavern.egg, TESTARN.EGG): order-independent per-section compare with font-rendered bitmap pixel rows excluded, ordinal art byte-exact vs TESTARN.EGG, EggFileMessage framing reassembly, role de-dup, No Return role semantics, and shipped-catalog contents. Invoker gains BTEggString/ BTEggFraming reflection cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
BattleTech data catalog for TeslaConsole (mirror of RedPlanet\RPConfig.xml).
|
||||
|
||||
Reconstructed from the Mac Console 4.10 `Console.ini` BattleTech tree
|
||||
(Adventure::Solo::BattleTech -> Location -> Scenario -> Time/Weather/
|
||||
Experience/Vehicle/Emblem/DropZone/Camo/Patch) and the two console-generated
|
||||
golden eggs (410console/battletech-port/reference/cavern.egg, TESTARN.EGG).
|
||||
|
||||
Wire (egg) mapping of the console fields:
|
||||
scenario -> [mission] scenario= (BOTH Free For All and No Return send "freeforall")
|
||||
map -> [mission] map=
|
||||
time -> [mission] time=
|
||||
weather -> [mission] weather= (temperature is a property of the weather entry)
|
||||
experience-> participant experience=
|
||||
vehicle -> participant vehicle= (the mech)
|
||||
color -> participant color= (this is the CAMO list)
|
||||
patch -> participant patch= (a color-name list, distinct from camo)
|
||||
badge -> participant badge= (the faction EMBLEM)
|
||||
role -> participant role=Role::<key>, plus a [Role::<key>] model=<model> block
|
||||
|
||||
Free For All and No Return differ ONLY by the pilot role (Default vs NoReturn);
|
||||
both emit scenario=freeforall on the wire. Image assets are added in a later
|
||||
phase (the image= attribute is optional; a missing one just shows the name).
|
||||
|
||||
Map-specific restrictions from the Mac ini that this flat model does NOT yet
|
||||
enforce (candidates for a later refinement): Cavern is night-only; Ravines has
|
||||
no "clear" weather (fog/soup only); Polar Missile Base uses 4 drop zones.
|
||||
-->
|
||||
<BattleTech>
|
||||
<freeforall name="Free For All">
|
||||
|
||||
<!-- Pilot skill. key = egg value; name = operator-facing label (Mac STR# 307). -->
|
||||
<experience key="novice" name="Standard" />
|
||||
<experience key="standard" name="Veteran" />
|
||||
<experience key="veteran" name="Expert" />
|
||||
|
||||
<!-- color= : mech camo (Mac BT::Standard::Camo::* tags). -->
|
||||
<color key="Grey" name="Grey" />
|
||||
<color key="White" name="White" />
|
||||
<color key="Black" name="Black" />
|
||||
<color key="Brown" name="Brown" />
|
||||
<color key="Crimson" name="Crimson" />
|
||||
<color key="Green" name="Green" />
|
||||
<color key="Tan" name="Tan" />
|
||||
|
||||
<!-- patch= : team/badge color (Mac BT::Standard::Badge* tags). -->
|
||||
<patch key="Red" name="Red" />
|
||||
<patch key="Yellow" name="Yellow" />
|
||||
<patch key="Blue" name="Blue" />
|
||||
<patch key="Green" name="Green" />
|
||||
<patch key="Grey" name="Grey" />
|
||||
<patch key="Violet" name="Violet" />
|
||||
<patch key="White" name="White" />
|
||||
<patch key="Black" name="Black" />
|
||||
|
||||
<!-- badge= : faction emblem (Mac BT::5DropZone::* emblem tags / STR# 3006). -->
|
||||
<badge key="VGL" name="VGL" />
|
||||
<badge key="Davion" name="Davion" />
|
||||
<badge key="Kurita" name="Kurita" />
|
||||
<badge key="Liao" name="Liao" />
|
||||
<badge key="Marik" name="Marik" />
|
||||
<badge key="Steiner" name="Steiner" />
|
||||
<badge key="None" name="None" />
|
||||
|
||||
<!-- role= : Free For All uses Default, No Return uses NoReturn. model= is the
|
||||
egg value (Mac ini tags dfltrole/noreturn; TESTARN.EGG emitted "noretun"
|
||||
for NoReturn, so that spelling is used as the pod consumed it). -->
|
||||
<role key="Default" model="dfltrole" name="Default" />
|
||||
<role key="NoReturn" model="noretun" name="No Return" />
|
||||
|
||||
</freeforall>
|
||||
|
||||
<!-- Arenas. key = egg map value (Mac tag=); name = Mac STR# 3000 label. -->
|
||||
<map key="arena1" name="Abandoned Arena" />
|
||||
<map key="arena2" name="Destroyed Arena" />
|
||||
<map key="polar3" name="Polar Missile Base" />
|
||||
<map key="polar4" name="Helicopter Base" />
|
||||
<map key="rav" name="Ravines" />
|
||||
<map key="grass" name="Savannah" />
|
||||
<map key="dbase" name="Badlands" />
|
||||
<map key="cavern" name="Cavern" />
|
||||
|
||||
<!-- Mechs. key = egg vehicle value (Mac tag=); name = Mac STR# 3002 label. -->
|
||||
<vehicle key="blkhawk" name="Blackhawk" />
|
||||
<vehicle key="loki" name="Loki" />
|
||||
<vehicle key="bhk1" name="Nova" />
|
||||
<vehicle key="madcat" name="Mad Cat" />
|
||||
<vehicle key="owens" name="Owens" />
|
||||
<vehicle key="thor" name="Thor" />
|
||||
<vehicle key="vulture" name="Vulture" />
|
||||
<vehicle key="avatar" name="Avatar" />
|
||||
<vehicle key="sunder" name="Sunder" />
|
||||
<vehicle key="mad1" name="Timber Wolf" />
|
||||
<vehicle key="ava1" name="Satyr" />
|
||||
<vehicle key="lok1" name="Hellbringer" />
|
||||
<vehicle key="thr1" name="Summoner" />
|
||||
<vehicle key="vul1" name="Maddog" />
|
||||
<vehicle key="own1" name="Kotori" />
|
||||
<vehicle key="snd1" name="Denkou" />
|
||||
<vehicle key="lok2" name="Mischief" />
|
||||
<vehicle key="mad2" name="Zanin Neko" />
|
||||
|
||||
<!-- Time of day (Mac STR# 301 / BT TimeList). -->
|
||||
<time key="morning" name="Morning" />
|
||||
<time key="day" name="Day" />
|
||||
<time key="evening" name="Evening" />
|
||||
<time key="night" name="Night" />
|
||||
|
||||
<!-- Weather. temperature is carried on the weather entry (both known BT weathers
|
||||
report 27 in the Mac ini); it becomes the egg [mission] temperature=. -->
|
||||
<weather key="clear" name="Clear" temperature="27" />
|
||||
<weather key="fog" name="Light Fog" temperature="27" />
|
||||
<weather key="soup" name="Heavy Fog" temperature="27" />
|
||||
|
||||
</BattleTech>
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace TeslaConsole.BattleTech;
|
||||
|
||||
/// <summary>
|
||||
/// A spectator / mission-review station. Mirrors <c>RPCamera</c>: 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 <c>team=</c> field) and should be confirmed against a live BT
|
||||
/// pod when one is available.
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using System.Xml;
|
||||
|
||||
namespace TeslaConsole.BattleTech;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and exposes the BattleTech data catalog (BattleTech\BTConfig.xml),
|
||||
/// mirroring <c>TeslaConsole.RedPlanet.RPConfig</c>. The single BT scenario is
|
||||
/// "Free For All"; No Return reuses the same catalog and differs only by role.
|
||||
/// </summary>
|
||||
public static class BTConfig
|
||||
{
|
||||
private static readonly BTScenario mFreeForAll;
|
||||
|
||||
private static readonly Dictionary<string, BTMap> mMaps;
|
||||
|
||||
private static readonly Dictionary<string, BTVehicle> mVehicles;
|
||||
|
||||
private static readonly Dictionary<string, string> mTimesOfDay;
|
||||
|
||||
private static readonly Dictionary<string, BTWeather> mWeather;
|
||||
|
||||
public static BTScenario FreeForAll => mFreeForAll;
|
||||
|
||||
public static Dictionary<string, BTMap> Maps => mMaps;
|
||||
|
||||
public static Dictionary<string, BTVehicle> Vehicles => mVehicles;
|
||||
|
||||
public static Dictionary<string, string> TimesOfDay => mTimesOfDay;
|
||||
|
||||
public static Dictionary<string, BTWeather> Weather => mWeather;
|
||||
|
||||
static BTConfig()
|
||||
{
|
||||
mMaps = new Dictionary<string, BTMap>();
|
||||
mVehicles = new Dictionary<string, BTVehicle>();
|
||||
mTimesOfDay = new Dictionary<string, string>();
|
||||
mWeather = new Dictionary<string, BTWeather>();
|
||||
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<string, string> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace TeslaConsole.BattleTech;
|
||||
|
||||
/// <summary>
|
||||
/// The BattleTech battle mission. Both operator game modes — Free For All and
|
||||
/// No Return — are produced by this class and send <c>scenario=freeforall</c> on
|
||||
/// the wire (confirmed by both golden eggs); the two modes differ only in the
|
||||
/// role assigned to each <see cref="BTPlayer" />.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
internal class BTFreeForAllMission : BTMission
|
||||
{
|
||||
public const string ScenarioTag = "freeforall";
|
||||
|
||||
private readonly List<BTPlayer> mPlayers = new List<BTPlayer>();
|
||||
|
||||
public List<BTPlayer> BattlePlayers => mPlayers;
|
||||
|
||||
public override IEnumerable<BTPlayer> 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Munga.Net;
|
||||
|
||||
namespace TeslaConsole.BattleTech;
|
||||
|
||||
/// <summary>
|
||||
/// A BattleTech mission and its egg serializer. Mirrors
|
||||
/// <c>TeslaConsole.RedPlanet.RPMission</c>: the wire framing
|
||||
/// (<see cref="ToEggFileMessages" />) and the plasma bitmap / ordinal sections
|
||||
/// are identical to Red Planet. BattleTech differs only in the egg body — see
|
||||
/// <see cref="ToEggString" /> (adventure name, non-zero temperature, no score
|
||||
/// compression, BT participant fields and the <c>[Role::*]</c> blocks).
|
||||
/// </summary>
|
||||
[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<BTCamera> mCameras = new List<BTCamera>();
|
||||
|
||||
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<BTPlayer> Players { get; }
|
||||
|
||||
public abstract int PlayerCount { get; }
|
||||
|
||||
public List<BTCamera> 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<string> list = new List<string>();
|
||||
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<BTRole> roles = new List<BTRole>();
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace TeslaConsole.BattleTech;
|
||||
|
||||
/// <summary>
|
||||
/// A BattleTech combatant (mech pilot). Mirrors <c>RPPlayer</c> 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
|
||||
/// <see cref="Role" />.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Xml;
|
||||
|
||||
namespace TeslaConsole.BattleTech;
|
||||
|
||||
/// <summary>
|
||||
/// A BattleTech pilot role. Each participant references one role
|
||||
/// (<c>role=Role::<Key></c>); the egg then carries a matching
|
||||
/// <c>[Role::<Key>] model=<Model></c> block. Free For All uses the
|
||||
/// Default role, No Return uses the NoReturn role.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
|
||||
namespace TeslaConsole.BattleTech;
|
||||
|
||||
/// <summary>
|
||||
/// A BattleTech scenario (game mode). Mirrors the Red Planet
|
||||
/// <c>Scenario</c>/<c>DeathRaceScenario</c> pattern: it inherits the shared
|
||||
/// map/vehicle/time/weather catalogs from <see cref="BTConfig" /> (minus any
|
||||
/// <c><invalid></c> exclusions) and adds the BT-specific option lists
|
||||
/// (experience, camo, patch, emblem, role).
|
||||
/// </summary>
|
||||
public class BTScenario
|
||||
{
|
||||
private readonly string mKey;
|
||||
|
||||
private readonly string mName;
|
||||
|
||||
private readonly Dictionary<string, BTMap> mMaps = new Dictionary<string, BTMap>();
|
||||
|
||||
private readonly Dictionary<string, BTVehicle> mVehicles = new Dictionary<string, BTVehicle>();
|
||||
|
||||
private readonly Dictionary<string, string> mTimesOfDay = new Dictionary<string, string>();
|
||||
|
||||
private readonly Dictionary<string, BTWeather> mWeather = new Dictionary<string, BTWeather>();
|
||||
|
||||
private readonly Dictionary<string, string> mExperiences = new Dictionary<string, string>();
|
||||
|
||||
private readonly Dictionary<string, string> mColors = new Dictionary<string, string>();
|
||||
|
||||
private readonly Dictionary<string, string> mPatches = new Dictionary<string, string>();
|
||||
|
||||
private readonly Dictionary<string, string> mBadges = new Dictionary<string, string>();
|
||||
|
||||
private readonly Dictionary<string, BTRole> mRoles = new Dictionary<string, BTRole>();
|
||||
|
||||
public string Key => mKey;
|
||||
|
||||
public string Name => mName;
|
||||
|
||||
public Dictionary<string, BTMap> Maps => mMaps;
|
||||
|
||||
public Dictionary<string, BTVehicle> Vehicles => mVehicles;
|
||||
|
||||
public Dictionary<string, string> TimesOfDay => mTimesOfDay;
|
||||
|
||||
public Dictionary<string, BTWeather> Weather => mWeather;
|
||||
|
||||
public Dictionary<string, string> Experiences => mExperiences;
|
||||
|
||||
public Dictionary<string, string> Colors => mColors;
|
||||
|
||||
public Dictionary<string, string> Patches => mPatches;
|
||||
|
||||
public Dictionary<string, string> Badges => mBadges;
|
||||
|
||||
public Dictionary<string, BTRole> Roles => mRoles;
|
||||
|
||||
internal BTScenario(XmlNode scenarioNode)
|
||||
{
|
||||
mKey = scenarioNode.Name;
|
||||
mName = scenarioNode.Attributes["name"].InnerText;
|
||||
List<KeyValuePair<string, string>> invalid = new List<KeyValuePair<string, string>>();
|
||||
foreach (XmlNode childNode in scenarioNode.ChildNodes)
|
||||
{
|
||||
switch (childNode.Name)
|
||||
{
|
||||
case "invalid":
|
||||
invalid.Add(new KeyValuePair<string, string>(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<string, string> 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<KeyValuePair<string, string>> invalid)
|
||||
{
|
||||
foreach (KeyValuePair<string, string> item in invalid)
|
||||
{
|
||||
if (item.Key == type && item.Value == value)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Xml;
|
||||
|
||||
namespace TeslaConsole.BattleTech;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>[mission] temperature=</c> field.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
<Content Include="RedPlanet\RPConfig.xml" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="RedPlanet\RPStrings.xml" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="RedPlanet\Apps.xml" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="BattleTech\BTConfig.xml" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using Xunit;
|
||||
|
||||
namespace TeslaConsole.DiffTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Golden-egg tests for the new TeslaConsole.BattleTech mission builder. BattleTech
|
||||
/// is absent from the original exe, so like CatalogTests these run against the
|
||||
/// recovered build only; the baselines are two eggs captured from the original
|
||||
/// consoles (BattleTech\cavern.egg, BattleTech\TESTARN.EGG — see the port spec in
|
||||
/// 410console/battletech-port/).
|
||||
///
|
||||
/// The comparison is per-section and order-independent: the pod parses the egg
|
||||
/// INI-style, and the two golden eggs themselves disagree with each other on
|
||||
/// participant field order. Pixel rows of the font-rendered name bitmaps are
|
||||
/// excluded — the original consoles rendered them with a different font, and
|
||||
/// cavern.egg's ordinal art is Mac-rendered too (the spec's known false
|
||||
/// byte-match). TESTARN.EGG's ordinal art, however, is identical to the
|
||||
/// RP-inherited rows, so that one IS compared byte-exactly.
|
||||
/// </summary>
|
||||
public class BTGoldenEggTests : IClassFixture<DifferentialFixture>
|
||||
{
|
||||
private readonly DifferentialFixture _fx;
|
||||
|
||||
public BTGoldenEggTests(DifferentialFixture fx) => _fx = fx;
|
||||
|
||||
// Mission args: mapKey, timeOfDayKey, weatherKey, temperature, lengthSeconds,
|
||||
// then per pilot: podHost, name, vehicle, camo, patch, emblem, experience,
|
||||
// dropzone, roleKey, roleModel (matches Invoker.BuildBTMission).
|
||||
private static readonly string[] CavernArgs =
|
||||
{
|
||||
"cavern", "night", "clear", "27", "120",
|
||||
"200.0.0.113", "cyd", "madcat", "Grey", "Red", "VGL", "veteran", "one", "Default", "dfltrole",
|
||||
};
|
||||
|
||||
private static readonly string[] TestarnArgs =
|
||||
{
|
||||
"arena1", "day", "clear", "27", "600",
|
||||
"200.0.0.113", "cyd", "madcat", "White", "Yellow", "VGL", "veteran", "one", "Default", "dfltrole",
|
||||
};
|
||||
|
||||
private string Generate(string[] args)
|
||||
{
|
||||
string egg = _fx.Recovered.Run("BTEggString", args);
|
||||
Assert.StartsWith("[mission]", egg); // not an "EXC:<Type>" result
|
||||
return egg;
|
||||
}
|
||||
|
||||
private static string ReadGolden(string fileName)
|
||||
{
|
||||
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "BattleTech", fileName);
|
||||
Assert.True(File.Exists(path), "Golden egg not found: " + path);
|
||||
// On disk the egg is the wire form: NUL-delimited ASCII.
|
||||
return Encoding.ASCII.GetString(File.ReadAllBytes(path));
|
||||
}
|
||||
|
||||
// ---- egg parsing ----
|
||||
|
||||
private static Dictionary<string, List<string>> ParseEgg(string text)
|
||||
{
|
||||
var sections = new Dictionary<string, List<string>>();
|
||||
List<string> current = null;
|
||||
foreach (var raw in text.Split(new[] { '\0', '\n' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
// TESTARN.EGG was captured with a stray \r before each NUL delimiter.
|
||||
var line = raw.TrimEnd('\r');
|
||||
if (line.Length == 0)
|
||||
continue;
|
||||
if (line.StartsWith("[", StringComparison.Ordinal))
|
||||
{
|
||||
string name = line.Trim('[', ']');
|
||||
Assert.False(sections.ContainsKey(name), "Duplicate egg section: " + name);
|
||||
current = new List<string>();
|
||||
sections.Add(name, current);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.NotNull(current); // a key=value before any section header
|
||||
current.Add(line);
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
/// <summary>Sections whose bitmap= lines are pixel rows, not references.</summary>
|
||||
private static bool IsPixelSection(string name)
|
||||
=> name.StartsWith("BitMap::", StringComparison.Ordinal) ||
|
||||
name.StartsWith("Ordinal::BitMap::", StringComparison.Ordinal);
|
||||
|
||||
private static Dictionary<string, List<string>> DropPixelRows(Dictionary<string, List<string>> egg)
|
||||
=> egg.ToDictionary(
|
||||
s => s.Key,
|
||||
s => IsPixelSection(s.Key)
|
||||
? s.Value.Where(l => !l.StartsWith("bitmap=", StringComparison.Ordinal)).ToList()
|
||||
: s.Value);
|
||||
|
||||
private static void AssertFieldsMatchGolden(string generated, string golden,
|
||||
params string[] acceptedExtraGoldenSections)
|
||||
{
|
||||
var gen = DropPixelRows(ParseEgg(generated));
|
||||
var gold = DropPixelRows(ParseEgg(golden));
|
||||
|
||||
var missing = gold.Keys.Except(gen.Keys).Except(acceptedExtraGoldenSections)
|
||||
.OrderBy(s => s, StringComparer.Ordinal).ToArray();
|
||||
var extra = gen.Keys.Except(gold.Keys)
|
||||
.OrderBy(s => s, StringComparer.Ordinal).ToArray();
|
||||
Assert.True(missing.Length == 0,
|
||||
"Golden egg sections missing from the generated egg: " + string.Join(", ", missing));
|
||||
Assert.True(extra.Length == 0,
|
||||
"Generated egg sections absent from the golden egg: " + string.Join(", ", extra));
|
||||
|
||||
foreach (var name in gen.Keys)
|
||||
{
|
||||
var g = gen[name].OrderBy(s => s, StringComparer.Ordinal).ToArray();
|
||||
var r = gold[name].OrderBy(s => s, StringComparer.Ordinal).ToArray();
|
||||
Assert.True(g.SequenceEqual(r),
|
||||
"[" + name + "] fields differ.\n golden: " + string.Join(" | ", r) +
|
||||
"\n generated: " + string.Join(" | ", g));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- golden-egg field equivalence ----
|
||||
|
||||
[Fact]
|
||||
public void Cavern_Egg_Fields_Match_Golden()
|
||||
=> AssertFieldsMatchGolden(Generate(CavernArgs), ReadGolden("cavern.egg"));
|
||||
|
||||
[Fact]
|
||||
public void Testarn_Egg_Fields_Match_Golden()
|
||||
{
|
||||
// The original console emitted a [Role::*] block for every role in its
|
||||
// catalog; ours emits only the roles the pilots reference, so TESTARN's
|
||||
// unreferenced [Role::NoReturn] is an accepted difference (extra blocks
|
||||
// are harmless to the pod's INI-style parser; missing referenced ones
|
||||
// would not be).
|
||||
AssertFieldsMatchGolden(Generate(TestarnArgs), ReadGolden("TESTARN.EGG"),
|
||||
"Role::NoReturn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ordinal_Bitmap_Art_Matches_Testarn_Byte_Exactly()
|
||||
{
|
||||
// TESTARN.EGG's 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -6,6 +6,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
|
||||
namespace TeslaConsole.DiffTests
|
||||
@@ -194,6 +195,12 @@ namespace TeslaConsole.DiffTests
|
||||
case "CatalogEntry":
|
||||
return CatalogEntry(args[0], args[1], args[2], args[3]);
|
||||
|
||||
case "BTEggString":
|
||||
return BTEggString(args);
|
||||
|
||||
case "BTEggFraming":
|
||||
return BTEggFraming(args);
|
||||
|
||||
default:
|
||||
throw new ArgumentException("Unknown case: " + caseName);
|
||||
}
|
||||
@@ -345,6 +352,88 @@ namespace TeslaConsole.DiffTests
|
||||
return $"{disp}|{lkey:D}|{exe}|{margs}|{wd}|{ar}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a TeslaConsole.BattleTech.BTFreeForAllMission from flat args:
|
||||
/// [0]=mapKey [1]=timeOfDayKey [2]=weatherKey [3]=temperature [4]=lengthSeconds,
|
||||
/// then one pilot per 10 args: podHost, name, vehicle, camo, patch, emblem,
|
||||
/// experience, dropzone, roleKey, roleModel. The BT types are internal to the
|
||||
/// exe, so construction goes through reflection like every other case.
|
||||
/// </summary>
|
||||
private object BuildBTMission(string[] args)
|
||||
{
|
||||
var missionType = T("TeslaConsole.BattleTech.BTFreeForAllMission");
|
||||
object mission = Activator.CreateInstance(missionType, new object[]
|
||||
{
|
||||
args[0], args[1], args[2],
|
||||
int.Parse(args[3], CultureInfo.InvariantCulture),
|
||||
TimeSpan.FromSeconds(double.Parse(args[4], CultureInfo.InvariantCulture)),
|
||||
});
|
||||
|
||||
var roleType = T("TeslaConsole.BattleTech.BTRole");
|
||||
var playerType = T("TeslaConsole.BattleTech.BTPlayer");
|
||||
object players = missionType.GetProperty("BattlePlayers").GetValue(mission, null);
|
||||
MethodInfo add = players.GetType().GetMethod("Add");
|
||||
for (int i = 5; i < args.Length; i += 10)
|
||||
{
|
||||
object role = Activator.CreateInstance(roleType,
|
||||
new object[] { args[i + 8], args[i + 9], args[i + 8] });
|
||||
object player = Activator.CreateInstance(playerType, new object[]
|
||||
{
|
||||
args[i], args[i + 1], args[i + 2], args[i + 3], args[i + 4],
|
||||
args[i + 5], args[i + 6], role,
|
||||
/* advancedDamage */ true, /* dropZoneKey */ args[i + 7],
|
||||
/* vehicleValue */ 0,
|
||||
});
|
||||
add.Invoke(players, new[] { player });
|
||||
}
|
||||
return mission;
|
||||
}
|
||||
|
||||
private string BTEggString(string[] args)
|
||||
{
|
||||
object mission = BuildBTMission(args);
|
||||
return (string)mission.GetType().GetMethod("ToEggString").Invoke(mission, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exercises BTMission.ToEggFileMessages and verifies the RP-inherited wire
|
||||
/// framing: newlines become NULs, ASCII bytes go out in 1000-byte chunks with
|
||||
/// contiguous sequence numbers, and the chunks reassemble byte-exactly to the
|
||||
/// ToEggString content. Returns "messages=N;total=L;framing=B;reassembled=B".
|
||||
/// </summary>
|
||||
private string BTEggFraming(string[] args)
|
||||
{
|
||||
object mission = BuildBTMission(args);
|
||||
Type mt = mission.GetType();
|
||||
string egg = (string)mt.GetMethod("ToEggString").Invoke(mission, null);
|
||||
byte[] expected = Encoding.ASCII.GetBytes(
|
||||
egg.Replace("\r\n", "\0").Replace('\n', '\0'));
|
||||
|
||||
var messages = (Array)mt.GetMethod("ToEggFileMessages").Invoke(mission, null);
|
||||
if (messages.Length == 0)
|
||||
return "no-messages";
|
||||
|
||||
Type et = messages.GetValue(0).GetType();
|
||||
FieldInfo seq = et.GetField("SequenceNumber");
|
||||
FieldInfo total = et.GetField("NotationFileLength");
|
||||
FieldInfo len = et.GetField("ThisMessageLength");
|
||||
FieldInfo data = et.GetField("NotationData");
|
||||
|
||||
bool framing = true;
|
||||
var reassembled = new MemoryStream();
|
||||
for (int i = 0; i < messages.Length; i++)
|
||||
{
|
||||
object m = messages.GetValue(i);
|
||||
int chunk = (int)len.GetValue(m);
|
||||
framing &= (int)seq.GetValue(m) == i;
|
||||
framing &= (int)total.GetValue(m) == expected.Length;
|
||||
framing &= chunk == (i < messages.Length - 1 ? 1000 : expected.Length - 1000 * i);
|
||||
reassembled.Write((byte[])data.GetValue(m), 0, chunk);
|
||||
}
|
||||
bool bytesMatch = reassembled.ToArray().SequenceEqual(expected);
|
||||
return $"messages={messages.Length};total={expected.Length};framing={framing};reassembled={bytesMatch}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic black/white bitmap whose pixels depend only on (x, y, seed),
|
||||
/// so both assemblies receive identical input pixels to pack.
|
||||
|
||||
@@ -27,7 +27,25 @@ proprietary dependencies — still resolves its references for metadata inspecti
|
||||
and property/event accessor methods are excluded — the README at the repo root
|
||||
notes those legitimately differ between a decompilation and the lost sources.
|
||||
|
||||
2. **Behavioral output** (`BehavioralEquivalenceTests`)
|
||||
2. **Recovered-only characterization** (`CatalogTests`, `BTGoldenEggTests`)
|
||||
Features that were *added* in the reconstruction have no counterpart in the
|
||||
original exe, so these run against the recovered build only:
|
||||
- `CatalogTests` — the data-driven product catalog reproduces the exact
|
||||
`LaunchData` the old hardcoded code emitted.
|
||||
- `BTGoldenEggTests` — the new `TeslaConsole.BattleTech` mission builder is
|
||||
diffed field-by-field against two golden eggs captured from the original
|
||||
consoles ([`BattleTech/cavern.egg`](BattleTech/cavern.egg),
|
||||
[`BattleTech/TESTARN.EGG`](BattleTech/TESTARN.EGG)). The comparison is
|
||||
per-section and order-independent (the pod parses eggs INI-style; the two
|
||||
golden eggs themselves disagree on field order). Font-rendered name-bitmap
|
||||
pixel rows are excluded, but TESTARN's ordinal art — identical to the
|
||||
RP-inherited rows — is compared byte-exactly. Also covers the
|
||||
`EggFileMessage` wire framing (NUL-delimited ASCII, 1000-byte chunks,
|
||||
byte-exact reassembly), role-block de-duplication, the No Return mode
|
||||
(same `scenario=freeforall`, different role), and the shipped
|
||||
`BattleTech\BTConfig.xml` catalog contents.
|
||||
|
||||
3. **Behavioral output** (`BehavioralEquivalenceTests`)
|
||||
The same deterministic, dependency-free methods are invoked in *both* assemblies
|
||||
over a battery of inputs and the results must match byte-for-byte:
|
||||
- `RPStrings.GetTimeString` (mm:ss formatting + 0.5 s rounding)
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- BattleTech golden eggs captured from the original consoles (see BTGoldenEggTests). -->
|
||||
<Content Include="BattleTech\*.egg" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--
|
||||
Build the reconstruction before the tests so we always compare against a fresh
|
||||
|
||||
Reference in New Issue
Block a user