dsda: initial support for boom demos
parser vastly rewritten to match upstream TODO: fix sync
This commit is contained in:
parent
d97fe551b8
commit
b2c33aa548
|
@ -7,54 +7,217 @@ namespace BizHawk.Client.Common
|
||||||
{
|
{
|
||||||
// LMP file format: https://doomwiki.org/wiki/Demo#Technical_information
|
// 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
|
// 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")]
|
[ImporterFor("Doom", ".lmp")]
|
||||||
internal class DoomLmpImport : MovieImporter
|
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()
|
protected override void RunImport()
|
||||||
{
|
{
|
||||||
var input = SourceFile.OpenRead().ReadAllBytes();
|
var input = SourceFile.OpenRead().ReadAllBytes();
|
||||||
var i = 0;
|
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.HeaderEntries[HeaderKeys.Core] = CoreNames.DSDA;
|
||||||
Result.Movie.SystemID = VSystemID.Raw.Doom;
|
Result.Movie.SystemID = VSystemID.Raw.Doom;
|
||||||
|
|
||||||
// Try to decide game version based on signature
|
// Try to decide game version
|
||||||
var signature = input[i];
|
var version = (DemoVersion)input[i++];
|
||||||
DSDA.CompatibilityLevel presumedCompatibilityLevel;
|
|
||||||
if (signature <= 102)
|
// 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
|
Result.Errors.Add($"Unknown demo format: {version}");
|
||||||
Console.WriteLine("Reading DOOM LMP demo version: <=1.12");
|
return;
|
||||||
presumedCompatibilityLevel = DSDA.CompatibilityLevel.Doom_12;
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (version < DemoVersion.Doom_1_4)
|
||||||
{
|
{
|
||||||
i++;
|
// there is no version, the first byte is the skill level
|
||||||
Console.WriteLine("Reading DOOM LMP demo version: {0}", signature);
|
skill = (DSDA.SkillLevel)version;
|
||||||
presumedCompatibilityLevel = signature < 109
|
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.Doom_1666
|
||||||
: DSDA.CompatibilityLevel.Doom2_19;
|
: 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++;
|
||||||
|
}
|
||||||
|
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()
|
DSDA.DoomSyncSettings syncSettings = new()
|
||||||
{
|
{
|
||||||
InputFormat = DoomControllerTypes.Doom,
|
InputFormat = DoomControllerTypes.Doom,
|
||||||
CompatibilityLevel = presumedCompatibilityLevel,
|
CompatibilityLevel = compLevel,
|
||||||
SkillLevel = (DSDA.SkillLevel) (1 + input[i++]),
|
SkillLevel = skill,
|
||||||
InitialEpisode = input[i++],
|
InitialEpisode = episode,
|
||||||
InitialMap = input[i++],
|
InitialMap = map,
|
||||||
MultiplayerMode = (DSDA.MultiplayerMode) input[i++],
|
MultiplayerMode = multiplayerMode,
|
||||||
MonstersRespawn = input[i++] is not 0,
|
MonstersRespawn = monstersRespawn,
|
||||||
FastMonsters = input[i++] is not 0,
|
FastMonsters = fastMonsters,
|
||||||
NoMonsters = input[i++] is not 0,
|
NoMonsters = noMonsters,
|
||||||
TurningResolution = DSDA.TurningResolution.Shorttics,
|
TurningResolution = turningResolution,
|
||||||
RenderWipescreen = false,
|
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.Player1Present = input[i++] is not 0;
|
||||||
syncSettings.Player2Present = input[i++] is not 0;
|
syncSettings.Player2Present = input[i++] is not 0;
|
||||||
syncSettings.Player3Present = input[i++] is not 0;
|
syncSettings.Player3Present = input[i++] is not 0;
|
||||||
syncSettings.Player4Present = 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);
|
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(syncSettings);
|
||||||
|
|
||||||
var doomController = new DoomControllerDeck(
|
var doomController = new DoomControllerDeck(
|
||||||
|
@ -63,33 +226,40 @@ namespace BizHawk.Client.Common
|
||||||
syncSettings.Player2Present,
|
syncSettings.Player2Present,
|
||||||
syncSettings.Player3Present,
|
syncSettings.Player3Present,
|
||||||
syncSettings.Player4Present,
|
syncSettings.Player4Present,
|
||||||
syncSettings.TurningResolution == DSDA.TurningResolution.Longtics);
|
turningResolution == DSDA.TurningResolution.Longtics);
|
||||||
|
|
||||||
var controller = new SimpleController(doomController.Definition);
|
var controller = new SimpleController(doomController.Definition);
|
||||||
controller.Definition.BuildMnemonicsCache(Result.Movie.SystemID);
|
controller.Definition.BuildMnemonicsCache(Result.Movie.SystemID);
|
||||||
Result.Movie.LogKey = Bk2LogEntryGenerator.GenerateLogKey(controller.Definition);
|
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($"P{port} Run Speed", unchecked((sbyte) input[i++]));
|
||||||
controller.AcceptNewAxis(playerPfx + "Strafing Speed", unchecked((sbyte) input[i++]));
|
controller.AcceptNewAxis($"P{port} Strafing Speed", unchecked((sbyte) input[i++]));
|
||||||
controller.AcceptNewAxis(playerPfx + "Turning 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++];
|
var buttons = input[i++];
|
||||||
controller[playerPfx + "Fire"] = (specialValue & 0b00000001) is not 0;
|
controller[$"P{port} Fire"] = (buttons & 0b00000001) is not 0;
|
||||||
controller[playerPfx + "Use"] = (specialValue & 0b00000010) is not 0;
|
controller[$"P{port} Use"] = (buttons & 0b00000010) is not 0;
|
||||||
bool changeWeapon = (specialValue & 0b00000100) is not 0;
|
var changeWeapon = (buttons & 0b00000100) is not 0;
|
||||||
int weapon = changeWeapon ? (((specialValue & 0b00111000) >> 3) + 1) : 0;
|
var weapon = changeWeapon ? (((buttons & 0b00111000) >> 3) + 1) : 0;
|
||||||
controller.AcceptNewAxis(playerPfx + "Weapon Select", weapon);
|
controller.AcceptNewAxis($"P{port} Weapon Select", weapon);
|
||||||
}
|
}
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if (syncSettings.Player1Present) ParsePlayer("P1 ");
|
if (syncSettings.Player1Present) ParsePlayer(1);
|
||||||
if (syncSettings.Player2Present) ParsePlayer("P2 ");
|
if (syncSettings.Player2Present) ParsePlayer(2);
|
||||||
if (syncSettings.Player3Present) ParsePlayer("P3 ");
|
if (syncSettings.Player3Present) ParsePlayer(3);
|
||||||
if (syncSettings.Player4Present) ParsePlayer("P4 ");
|
if (syncSettings.Player4Present) ParsePlayer(4);
|
||||||
|
|
||||||
Result.Movie.AppendFrame(controller);
|
Result.Movie.AppendFrame(controller);
|
||||||
|
|
||||||
if (i == input.Length) throw new Exception("Reached end of input movie stream without finalization byte");
|
if (i == input.Length) throw new Exception("Reached end of input movie stream without finalization byte");
|
||||||
}
|
}
|
||||||
while (input[i] is not 0x80);
|
while (input[i] is not 0x80);
|
||||||
|
|
Loading…
Reference in New Issue