dsda: initial support for boom demos

parser vastly rewritten to match upstream
TODO: fix sync
This commit is contained in:
feos 2025-05-20 22:10:00 +03:00
parent d97fe551b8
commit b2c33aa548
1 changed files with 206 additions and 36 deletions

View File

@ -7,54 +7,217 @@ namespace BizHawk.Client.Common
{
// LMP file format: https://doomwiki.org/wiki/Demo#Technical_information
// In better detail, from archive.org: http://web.archive.org/web/20070630072856/http://demospecs.planetquake.gamespy.com/lmp/lmp.html
// https://www.doomworld.com/forum/topic/120007-specifications-for-source-port-demo-formats
[ImporterFor("Doom", ".lmp")]
internal class DoomLmpImport : MovieImporter
{
private enum DemoVersion : int
{
Skill_1 = 0,
Skill_5 = 4,
Doom_1_4 = 104, // first Doom to write version to demo
Doom_1_666 = 106,
Doom_1_9 = 109, // Doom/Doom2/Ultimate/Final
TASDoom = 110,
DoomClassic = 111, // first longtics support
Boom_2_00 = 200,
Boom_2_01 = 201,
Boom_2_02 = 202,
MBF = 203, // LxDoom/MBF
PrBoom_2_1_0 = 210,
// this matching looks weird but it's how DSDA-Doom parses them
PrBoom_2_2_x = 211,
PrBoom_2_3_x = 212,
PrBoom_2_4_0 = 213,
PrBoomPlus = 214,
MBF21 = 221,
}
protected override void RunImport()
{
var input = SourceFile.OpenRead().ReadAllBytes();
var i = 0;
// version dependent settings
var compLevel = DSDA.CompatibilityLevel.MBF21;
var turningResolution = DSDA.TurningResolution.Shorttics;
var skill = DSDA.SkillLevel.UV;
var episode = 1;
var map = 0;
// v1.2- demos didn't store these (nor DisplayPlayer), they have to be explicitly set
var multiplayerMode = DSDA.MultiplayerMode.Single_Coop;
var monstersRespawn = false;
var fastMonsters = false;
var noMonsters = false;
Result.Movie.HeaderEntries[HeaderKeys.Core] = CoreNames.DSDA;
Result.Movie.SystemID = VSystemID.Raw.Doom;
// Try to decide game version based on signature
var signature = input[i];
DSDA.CompatibilityLevel presumedCompatibilityLevel;
if (signature <= 102)
// Try to decide game version
var version = (DemoVersion)input[i++];
// Handling of unrecognized demo formats
// Versions up to 1.2 use a 7-byte header - first byte is a skill level.
// Versions after 1.2 use a 13-byte header - first byte is a demoversion.
// BOOM's demoversion starts from 200
if (!((version >= DemoVersion.Skill_1 && version <= DemoVersion.Skill_5 ) ||
(version >= DemoVersion.Doom_1_4 && version <= DemoVersion.DoomClassic) ||
(version >= DemoVersion.Boom_2_00 && version <= DemoVersion.PrBoomPlus ) ||
(version == DemoVersion.MBF21)))
{
// there is no signature, the first byte is the skill level, so don't advance
Console.WriteLine("Reading DOOM LMP demo version: <=1.12");
presumedCompatibilityLevel = DSDA.CompatibilityLevel.Doom_12;
Result.Errors.Add($"Unknown demo format: {version}");
return;
}
if (version < DemoVersion.Doom_1_4)
{
// there is no version, the first byte is the skill level
skill = (DSDA.SkillLevel)version;
episode = input[i++];
map = input[i++];
compLevel = DSDA.CompatibilityLevel.Doom_12;
Console.WriteLine("Reading DOOM LMP demo version: 1.2-");
}
else if (version < DemoVersion.Boom_2_00)
{
if (version == DemoVersion.TASDoom)
{
compLevel = DSDA.CompatibilityLevel.TasDoom;
}
else if (version >= DemoVersion.DoomClassic)
{
turningResolution = DSDA.TurningResolution.Longtics;
}
skill = (DSDA.SkillLevel) (input[i++] + 1);
episode = input[i++];
map = input[i++];
multiplayerMode = (DSDA.MultiplayerMode) input[i++];
monstersRespawn = input[i++] is not 0;
fastMonsters = input[i++] is not 0;
noMonsters = input[i++] is not 0;
i++; // DisplayPlayer is a non-sync setting so importers can't set it
// DSDA-Doom assumes 1.666 compat for sig < 107 but this should be fine too
compLevel = version < DemoVersion.Doom_1_9
? DSDA.CompatibilityLevel.Doom_1666
: DSDA.CompatibilityLevel.Doom2_19;
Console.WriteLine("Reading DOOM LMP demo version: {0}", version);
}
else // Boom territory
{
i++; // skip to signature's second byte
var portID = input[i++];
i += 4; // skip the rest of the signature
switch (version)
{
case DemoVersion.Boom_2_00:
case DemoVersion.Boom_2_01:
if (input[i++] == 1)
{
compLevel = DSDA.CompatibilityLevel.Boom_Compatibility;
}
else
{
compLevel = DSDA.CompatibilityLevel.Boom_201;
}
break;
case DemoVersion.Boom_2_02:
if (input[i++] == 1)
{
compLevel = DSDA.CompatibilityLevel.Boom_Compatibility;
}
else
{
compLevel = DSDA.CompatibilityLevel.Boom_202;
}
break;
case DemoVersion.MBF:
if (portID == (byte) 'B') // "BOOM"
{
// don't advance!
compLevel = DSDA.CompatibilityLevel.LxDoom;
}
else if (portID == (byte) 'M') // "MBF"
{
compLevel = DSDA.CompatibilityLevel.MBF21;
i++;
Console.WriteLine("Reading DOOM LMP demo version: {0}", signature);
presumedCompatibilityLevel = signature < 109
? DSDA.CompatibilityLevel.Doom_1666
: DSDA.CompatibilityLevel.Doom2_19;
}
break;
case DemoVersion.PrBoom_2_1_0:
compLevel = DSDA.CompatibilityLevel.PrBoom_2;
i++;
break;
case DemoVersion.PrBoom_2_2_x:
compLevel = DSDA.CompatibilityLevel.PrBoom_3;
i++;
break;
case DemoVersion.PrBoom_2_3_x:
compLevel = DSDA.CompatibilityLevel.PrBoom_4;
i++;
break;
case DemoVersion.PrBoom_2_4_0:
compLevel = DSDA.CompatibilityLevel.PrBoom_5;
i++;
break;
case DemoVersion.PrBoomPlus:
compLevel = DSDA.CompatibilityLevel.PrBoom_6;
turningResolution = DSDA.TurningResolution.Longtics;
i++;
break;
case DemoVersion.MBF21:
compLevel = DSDA.CompatibilityLevel.MBF21;
turningResolution = DSDA.TurningResolution.Longtics;
i++;
break;
default:
Result.Errors.Add($"Unknown demo format: {version}");
return;
}
skill = (DSDA.SkillLevel) (input[i++] + 1);
episode = input[i++];
map = input[i++];
multiplayerMode = (DSDA.MultiplayerMode) input[i++];
i++; // DisplayPlayer is a non-sync setting so importers can't set it
}
DSDA.DoomSyncSettings syncSettings = new()
{
InputFormat = DoomControllerTypes.Doom,
CompatibilityLevel = presumedCompatibilityLevel,
SkillLevel = (DSDA.SkillLevel) (1 + input[i++]),
InitialEpisode = input[i++],
InitialMap = input[i++],
MultiplayerMode = (DSDA.MultiplayerMode) input[i++],
MonstersRespawn = input[i++] is not 0,
FastMonsters = input[i++] is not 0,
NoMonsters = input[i++] is not 0,
TurningResolution = DSDA.TurningResolution.Shorttics,
CompatibilityLevel = compLevel,
SkillLevel = skill,
InitialEpisode = episode,
InitialMap = map,
MultiplayerMode = multiplayerMode,
MonstersRespawn = monstersRespawn,
FastMonsters = fastMonsters,
NoMonsters = noMonsters,
TurningResolution = turningResolution,
RenderWipescreen = false,
};
_ = input[i++]; // DisplayPlayer is a non-sync setting so importers can't* set it
if (version >= DemoVersion.Boom_2_00)
{
var optionsSize = compLevel == DSDA.CompatibilityLevel.MBF21 ? 21 + 25 : 64;
i += optionsSize;
if (version == DemoVersion.Boom_2_00)
i += 256 - optionsSize;
}
syncSettings.Player1Present = input[i++] is not 0;
syncSettings.Player2Present = input[i++] is not 0;
syncSettings.Player3Present = input[i++] is not 0;
syncSettings.Player4Present = input[i++] is not 0;
if (compLevel >= DSDA.CompatibilityLevel.Boom_Compatibility
&& version >= DemoVersion.Boom_2_00)
{
var FUTURE_MAXPLAYERS = 32;
var g_maxplayers = 4;
i += FUTURE_MAXPLAYERS - g_maxplayers;
}
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(syncSettings);
var doomController = new DoomControllerDeck(
@ -63,33 +226,40 @@ namespace BizHawk.Client.Common
syncSettings.Player2Present,
syncSettings.Player3Present,
syncSettings.Player4Present,
syncSettings.TurningResolution == DSDA.TurningResolution.Longtics);
turningResolution == DSDA.TurningResolution.Longtics);
var controller = new SimpleController(doomController.Definition);
controller.Definition.BuildMnemonicsCache(Result.Movie.SystemID);
Result.Movie.LogKey = Bk2LogEntryGenerator.GenerateLogKey(controller.Definition);
void ParsePlayer(string playerPfx)
void ParsePlayer(int port)
{
controller.AcceptNewAxis(playerPfx + "Run Speed", unchecked((sbyte) input[i++]));
controller.AcceptNewAxis(playerPfx + "Strafing Speed", unchecked((sbyte) input[i++]));
controller.AcceptNewAxis(playerPfx + "Turning Speed", unchecked((sbyte) input[i++]));
controller.AcceptNewAxis($"P{port} Run Speed", unchecked((sbyte) input[i++]));
controller.AcceptNewAxis($"P{port} Strafing Speed", unchecked((sbyte) input[i++]));
if (turningResolution == DSDA.TurningResolution.Longtics)
{
// low byte comes first and is stored as an unsigned value
controller.AcceptNewAxis($"P{port} Turning Speed Frac.", unchecked((byte) input[i++]));
}
controller.AcceptNewAxis($"P{port} Turning Speed", unchecked((sbyte) input[i++]));
var specialValue = input[i++];
controller[playerPfx + "Fire"] = (specialValue & 0b00000001) is not 0;
controller[playerPfx + "Use"] = (specialValue & 0b00000010) is not 0;
bool changeWeapon = (specialValue & 0b00000100) is not 0;
int weapon = changeWeapon ? (((specialValue & 0b00111000) >> 3) + 1) : 0;
controller.AcceptNewAxis(playerPfx + "Weapon Select", weapon);
var buttons = input[i++];
controller[$"P{port} Fire"] = (buttons & 0b00000001) is not 0;
controller[$"P{port} Use"] = (buttons & 0b00000010) is not 0;
var changeWeapon = (buttons & 0b00000100) is not 0;
var weapon = changeWeapon ? (((buttons & 0b00111000) >> 3) + 1) : 0;
controller.AcceptNewAxis($"P{port} Weapon Select", weapon);
}
do
{
if (syncSettings.Player1Present) ParsePlayer("P1 ");
if (syncSettings.Player2Present) ParsePlayer("P2 ");
if (syncSettings.Player3Present) ParsePlayer("P3 ");
if (syncSettings.Player4Present) ParsePlayer("P4 ");
if (syncSettings.Player1Present) ParsePlayer(1);
if (syncSettings.Player2Present) ParsePlayer(2);
if (syncSettings.Player3Present) ParsePlayer(3);
if (syncSettings.Player4Present) ParsePlayer(4);
Result.Movie.AppendFrame(controller);
if (i == input.Length) throw new Exception("Reached end of input movie stream without finalization byte");
}
while (input[i] is not 0x80);