Convert fcm importer to new style, input parsing seems to have been broken this whole time, this commit doesn't fix that, just converts it

This commit is contained in:
adelikat 2019-11-10 09:55:27 -06:00
parent d0f54f88ee
commit 6d535a11ee
4 changed files with 309 additions and 286 deletions

View File

@ -154,6 +154,7 @@
<DependentUpon>Bk2Movie.cs</DependentUpon>
</Compile>
<Compile Include="movie\bk2\StringLogs.cs" />
<Compile Include="movie\import\FcmImport.cs" />
<Compile Include="movie\import\PxmImport.cs" />
<Compile Include="movie\tasproj\IStateManager.cs" />
<Compile Include="movie\tasproj\StateManagerDecay.cs" />

View File

@ -0,0 +1,305 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using BizHawk.Common.BufferExtensions;
using BizHawk.Emulation.Cores.Nintendo.NES;
namespace BizHawk.Client.Common.movie.import
{
// FCM file format: http://code.google.com/p/fceu/wiki/FCM
// ReSharper disable once UnusedMember.Global
[ImportExtension(".fcm")]
public class FcmImport : MovieImporter
{
private IControllerDeck _deck;
protected override void RunImport()
{
using var r = new BinaryReader(SourceFile.Open(FileMode.Open, FileAccess.Read));
var signature = new string(r.ReadChars(4));
if (signature != "FCM\x1A")
{
Result.Errors.Add("This is not a valid .FCM file.");
return;
}
Result.Movie.HeaderEntries[HeaderKeys.PLATFORM] = "NES";
var syncSettings = new NES.NESSyncSettings();
var controllerSettings = new NESControlSettings
{
NesLeftPort = nameof(ControllerNES),
NesRightPort = nameof(ControllerNES)
};
_deck = controllerSettings.Instantiate((x, y) => true);
AddDeckControlButtons();
// 004 4-byte little-endian unsigned int: version number, must be 2
uint version = r.ReadUInt32();
if (version != 2)
{
Result.Errors.Add(".FCM movie version must always be 2.");
return;
}
Result.Movie.Comments.Add($"{MovieOrigin} .FCM version {version}");
// 008 1-byte flags
byte flags = r.ReadByte();
/*
* bit 0: reserved, set to 0
* bit 1:
* if "0", movie begins from an embedded "quicksave" snapshot
* if "1", movie begins from reset or power-on[1]
*/
if (((flags >> 1) & 0x1) == 0)
{
Result.Errors.Add("Movies that begin with a savestate are not supported.");
return;
}
/*
bit 2:
* if "0", NTSC timing
* if "1", "PAL" timing
Starting with version 0.98.12 released on September 19, 2004, a "PAL" flag was added to the header but
unfortunately it is not reliable - the emulator does not take the "PAL" setting from the ROM, but from a user
preference. This means that this site cannot calculate movie lengths reliably.
*/
bool pal = ((flags >> 2) & 0x1) != 0;
Result.Movie.HeaderEntries[HeaderKeys.PAL] = pal.ToString();
// other: reserved, set to 0
bool syncHack = ((flags >> 4) & 0x1) != 0;
Result.Movie.Comments.Add($"SyncHack {syncHack}");
// 009 1-byte flags: reserved, set to 0
r.ReadByte();
// 00A 1-byte flags: reserved, set to 0
r.ReadByte();
// 00B 1-byte flags: reserved, set to 0
r.ReadByte();
// 00C 4-byte little-endian unsigned int: number of frames
uint frameCount = r.ReadUInt32();
// 010 4-byte little-endian unsigned int: rerecord count
uint rerecordCount = r.ReadUInt32();
Result.Movie.Rerecords = rerecordCount;
/*
018 4-byte little-endian unsigned int: offset to the savestate inside file
The savestate offset is <header_size + length_of_metadata_in_bytes + padding>. The savestate offset should be
4-byte aligned. At the savestate offset there is a savestate file. The savestate exists even if the movie is
reset-based.
*/
r.ReadUInt32();
// 01C 4-byte little-endian unsigned int: offset to the controller data inside file
uint firstFrameOffset = r.ReadUInt32();
// 020 16-byte md5sum of the ROM used
byte[] md5 = r.ReadBytes(16);
Result.Movie.HeaderEntries[MD5] = md5.BytesToHexString().ToLower();
// 030 4-byte little-endian unsigned int: version of the emulator used
uint emuVersion = r.ReadUInt32();
Result.Movie.Comments.Add($"{EmulationOrigin} FCEU {emuVersion}");
// 034 name of the ROM used - UTF8 encoded nul-terminated string.
var gameBytes = new List<byte>();
while (r.PeekChar() != 0)
{
gameBytes.Add(r.ReadByte());
}
// Advance past null byte.
r.ReadByte();
string gameName = Encoding.UTF8.GetString(gameBytes.ToArray());
Result.Movie.HeaderEntries[HeaderKeys.GAMENAME] = gameName;
/*
After the header comes "metadata", which is UTF8-coded movie title string. The metadata begins after the ROM
name and ends at the savestate offset. This string is displayed as "Author Info" in the Windows version of the
emulator.
*/
var authorBytes = new List<byte>();
while (r.PeekChar() != 0)
{
authorBytes.Add(r.ReadByte());
}
// Advance past null byte.
r.ReadByte();
string author = Encoding.UTF8.GetString(authorBytes.ToArray());
Result.Movie.HeaderEntries[HeaderKeys.AUTHOR] = author;
// Advance to first byte of input data.
r.BaseStream.Position = firstFrameOffset;
var controllers = new SimpleController
{
Definition = _deck.GetDefinition()
};
string[] buttons = { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" };
bool fds = false;
bool fourscore = false;
int frame = 1;
while (frame <= frameCount)
{
byte update = r.ReadByte();
// aa: Number of delta bytes to follow
int delta = (update >> 5) & 0x3;
int frames = 0;
/*
The delta byte(s) indicate the number of emulator frames between this update and the next update. It is
encoded in little-endian format and its size depends on the magnitude of the delta:
Delta of: Number of bytes:
0 0
1-255 1
256-65535 2
65536-(2^24-1) 3
*/
for (int b = 0; b < delta; b++)
{
frames += r.ReadByte() * (int)Math.Pow(2, b * 8);
}
frame += frames;
while (frames > 0)
{
Result.Movie.AppendFrame(controllers);
if (controllers["Reset"])
{
controllers["Reset"] = false;
}
frames--;
}
if (((update >> 7) & 0x1) != 0)
{
// Control update: 0x1aabbbbb
bool reset = false;
int command = update & 0x1F;
// 0xbbbbb:
controllers["Reset"] = command == 1;
switch (command)
{
case 0: // Do nothing
break;
case 1: // Reset
reset = true;
break;
case 2: // Power cycle
reset = true;
if (frame != 1)
{
controllers["Power"] = true;
}
break;
case 7: // VS System Insert Coin
Result.Warnings.Add($"Unsupported command: VS System Insert Coin at frame {frame}");
break;
case 8: // VS System Dipswitch 0 Toggle
Result.Warnings.Add($"Unsupported command: VS System Dipswitch 0 Toggle at frame {frame}");
break;
case 24: // FDS Insert
fds = true;
Result.Warnings.Add($"Unsupported command: FDS Insert at frame {frame}");
break;
case 25: // FDS Eject
fds = true;
Result.Warnings.Add($"Unsupported command: FDS Eject at frame {frame}");
break;
case 26: // FDS Select Side
fds = true;
Result.Warnings.Add($"Unsupported command: FDS Select Side at frame {frame}");
break;
default:
Result.Warnings.Add($"Unknown command: {command} detected at frame {frame}");
break;
}
/*
1 Even if the header says "movie begins from reset", the file still contains a quicksave, and the
quicksave is actually loaded. This flag can't therefore be trusted. To check if the movie actually
begins from reset, one must analyze the controller data and see if the first non-idle command in the
file is a Reset or Power Cycle type control command.
*/
if (!reset && frame == 1)
{
Result.Errors.Add("Movies that begin with a savestate are not supported.");
return;
}
}
else
{
/*
Controller update: 0aabbccc
* bb: Gamepad number minus one (?)
*/
int player = ((update >> 3) & 0x3) + 1;
if (player > 2)
{
Result.Errors.Add("Four score not yet supported.");
return;
}
/*
ccc:
* 0 A
* 1 B
* 2 Select
* 3 Start
* 4 Up
* 5 Down
* 6 Left
* 7 Right
*/
int button = update & 0x7;
/*
The controller update toggles the affected input. Controller update data is emitted to the movie file
only when the state of the controller changes.
*/
controllers[$"P{player} {buttons[button]}"] = !controllers[$"P{player} {buttons[button]}"];
}
Result.Movie.AppendFrame(controllers);
}
if (fds)
{
Result.Movie.HeaderEntries[HeaderKeys.BOARDNAME] = "FDS";
}
syncSettings.Controls = controllerSettings;
Result.Movie.SyncSettingsJson = ToJson(syncSettings);
}
private void AddDeckControlButtons()
{
var controllers = new SimpleController
{
Definition = _deck.GetDefinition()
};
// TODO: FDS
// Yes, this adds them to the deck definition too
controllers.Definition.BoolButtons.Add("Reset");
controllers.Definition.BoolButtons.Add("Power");
}
}
}

View File

@ -148,9 +148,6 @@ namespace BizHawk.Client.Common
{
switch (ext)
{
case ".FCM":
m = ImportFcm(path, out errorMsg, out warningMsg);
break;
case ".FMV":
m = ImportFmv(path, out errorMsg, out warningMsg);
break;
@ -227,7 +224,7 @@ namespace BizHawk.Client.Common
{
string[] extensions =
{
"BKM", "FCM", "FMV", "GMV", "MCM", "MC2", "MMV", "NMV", "LSMV", "SMV", "VBM", "VMV", "YMV", "ZMV"
"BKM", "FMV", "GMV", "MCM", "MC2", "MMV", "NMV", "LSMV", "SMV", "VBM", "VMV", "YMV", "ZMV"
};
return extensions.Any(ext => extension.ToUpper() == $".{ext}");
}
@ -428,10 +425,6 @@ namespace BizHawk.Client.Common
var platform = "";
switch (Path.GetExtension(path).ToUpper())
{
case ".FM2":
emulator = "FCEUX";
platform = "NES";
break;
case ".MC2":
emulator = "Mednafen/PCEjin";
platform = "PCE";
@ -609,284 +602,6 @@ namespace BizHawk.Client.Common
return str;
}
// FCM file format: http://code.google.com/p/fceu/wiki/FCM
private static BkmMovie ImportFcm(string path, out string errorMsg, out string warningMsg)
{
errorMsg = warningMsg = "";
BkmMovie m = new BkmMovie(path);
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
BinaryReader r = new BinaryReader(fs);
// 000 4-byte signature: 46 43 4D 1A "FCM\x1A"
string signature = r.ReadStringFixedAscii(4);
if (signature != "FCM\x1A")
{
errorMsg = "This is not a valid .FCM file.";
r.Close();
fs.Close();
return null;
}
// 004 4-byte little-endian unsigned int: version number, must be 2
uint version = r.ReadUInt32();
if (version != 2)
{
errorMsg = ".FCM movie version must always be 2.";
r.Close();
fs.Close();
return null;
}
m.Comments.Add($"{MOVIEORIGIN} .FCM version {version}");
// 008 1-byte flags
byte flags = r.ReadByte();
/*
* bit 0: reserved, set to 0
* bit 1:
* if "0", movie begins from an embedded "quicksave" snapshot
* if "1", movie begins from reset or power-on[1]
*/
if (((flags >> 1) & 0x1) == 0)
{
errorMsg = "Movies that begin with a savestate are not supported.";
r.Close();
fs.Close();
return null;
}
/*
bit 2:
* if "0", NTSC timing
* if "1", "PAL" timing
Starting with version 0.98.12 released on September 19, 2004, a "PAL" flag was added to the header but
unfortunately it is not reliable - the emulator does not take the "PAL" setting from the ROM, but from a user
preference. This means that this site cannot calculate movie lengths reliably.
*/
bool pal = ((flags >> 2) & 0x1) != 0;
m.Header[HeaderKeys.PAL] = pal.ToString();
// other: reserved, set to 0
bool syncHack = ((flags >> 4) & 0x1) != 0;
m.Comments.Add($"{SYNCHACK} {syncHack}");
// 009 1-byte flags: reserved, set to 0
r.ReadByte();
// 00A 1-byte flags: reserved, set to 0
r.ReadByte();
// 00B 1-byte flags: reserved, set to 0
r.ReadByte();
// 00C 4-byte little-endian unsigned int: number of frames
uint frameCount = r.ReadUInt32();
// 010 4-byte little-endian unsigned int: rerecord count
uint rerecordCount = r.ReadUInt32();
m.Rerecords = rerecordCount;
/*
018 4-byte little-endian unsigned int: offset to the savestate inside file
The savestate offset is <header_size + length_of_metadata_in_bytes + padding>. The savestate offset should be
4-byte aligned. At the savestate offset there is a savestate file. The savestate exists even if the movie is
reset-based.
*/
r.ReadUInt32();
// 01C 4-byte little-endian unsigned int: offset to the controller data inside file
uint firstFrameOffset = r.ReadUInt32();
// 020 16-byte md5sum of the ROM used
byte[] md5 = r.ReadBytes(16);
m.Header[MD5] = md5.BytesToHexString().ToLower();
// 030 4-byte little-endian unsigned int: version of the emulator used
uint emuVersion = r.ReadUInt32();
m.Comments.Add($"{EMULATIONORIGIN} FCEU {emuVersion}");
// 034 name of the ROM used - UTF8 encoded nul-terminated string.
List<byte> gameBytes = new List<byte>();
while (r.PeekChar() != 0)
{
gameBytes.Add(r.ReadByte());
}
// Advance past null byte.
r.ReadByte();
string gameName = Encoding.UTF8.GetString(gameBytes.ToArray());
m.Header[HeaderKeys.GAMENAME] = gameName;
/*
After the header comes "metadata", which is UTF8-coded movie title string. The metadata begins after the ROM
name and ends at the savestate offset. This string is displayed as "Author Info" in the Windows version of the
emulator.
*/
List<byte> authorBytes = new List<byte>();
while (r.PeekChar() != 0)
{
authorBytes.Add(r.ReadByte());
}
// Advance past null byte.
r.ReadByte();
string author = Encoding.UTF8.GetString(authorBytes.ToArray());
m.Header[HeaderKeys.AUTHOR] = author;
// Advance to first byte of input data.
r.BaseStream.Position = firstFrameOffset;
SimpleController controllers = new SimpleController { Definition = new ControllerDefinition { Name = "NES Controller" } };
string[] buttons = { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" };
bool fds = false;
bool fourscore = false;
int frame = 1;
while (frame <= frameCount)
{
byte update = r.ReadByte();
// aa: Number of delta bytes to follow
int delta = (update >> 5) & 0x3;
int frames = 0;
/*
The delta byte(s) indicate the number of emulator frames between this update and the next update. It is
encoded in little-endian format and its size depends on the magnitude of the delta:
Delta of: Number of bytes:
0 0
1-255 1
256-65535 2
65536-(2^24-1) 3
*/
for (int b = 0; b < delta; b++)
{
frames += r.ReadByte() * (int)Math.Pow(2, b * 8);
}
frame += frames;
while (frames > 0)
{
m.AppendFrame(controllers);
if (controllers["Reset"])
{
controllers["Reset"] = false;
}
frames--;
}
if (((update >> 7) & 0x1) != 0)
{
// Control update: 1aabbbbb
bool reset = false;
int command = update & 0x1F;
// bbbbb:
controllers["Reset"] = command == 1;
if (warningMsg == "")
{
switch (command)
{
case 0: // Do nothing
break;
case 1: // Reset
reset = true;
break;
case 2: // Power cycle
reset = true;
if (frame != 1)
{
warningMsg = "hard reset";
}
break;
case 7: // VS System Insert Coin
warningMsg = "VS System Insert Coin";
break;
case 8: // VS System Dipswitch 0 Toggle
warningMsg = "VS System Dipswitch 0 Toggle";
break;
case 24: // FDS Insert
fds = true;
warningMsg = "FDS Insert";
break;
case 25: // FDS Eject
fds = true;
warningMsg = "FDS Eject";
break;
case 26: // FDS Select Side
fds = true;
warningMsg = "FDS Select Side";
break;
default:
warningMsg = "unknown";
break;
}
if (warningMsg != "")
{
warningMsg = $"Unable to import {warningMsg} command at frame {frame}.";
}
}
/*
1 Even if the header says "movie begins from reset", the file still contains a quicksave, and the
quicksave is actually loaded. This flag can't therefore be trusted. To check if the movie actually
begins from reset, one must analyze the controller data and see if the first non-idle command in the
file is a Reset or Power Cycle type control command.
*/
if (!reset && frame == 1)
{
errorMsg = "Movies that begin with a savestate are not supported.";
r.Close();
fs.Close();
return null;
}
}
else
{
/*
Controller update: 0aabbccc
* bb: Gamepad number minus one (?)
*/
int player = ((update >> 3) & 0x3) + 1;
if (player > 2)
{
fourscore = true;
}
/*
ccc:
* 0 A
* 1 B
* 2 Select
* 3 Start
* 4 Up
* 5 Down
* 6 Left
* 7 Right
*/
int button = update & 0x7;
/*
The controller update toggles the affected input. Controller update data is emitted to the movie file
only when the state of the controller changes.
*/
controllers[$"P{player} {buttons[button]}"] = !controllers[$"P{player} {buttons[button]}"];
}
}
m.Header[HeaderKeys.PLATFORM] = "NES";
if (fds)
{
m.Header[HeaderKeys.BOARDNAME] = "FDS";
}
m.Header[HeaderKeys.FOURSCORE] = fourscore.ToString();
r.Close();
fs.Close();
return m;
}
// FMV file format: http://tasvideos.org/FMV.html
private static BkmMovie ImportFmv(string path, out string errorMsg, out string warningMsg)
{

View File

@ -201,6 +201,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dega/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=delaminated/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dendy/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dipswitch/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Disasm/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Disassemblable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=disassembly/@EntryIndexedValue">True</s:Boolean>
@ -264,6 +265,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Phaser/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pollable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Prereqs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=quicksave/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Regionable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=resizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rewinder/@EntryIndexedValue">True</s:Boolean>