182 lines
5.0 KiB
C#
182 lines
5.0 KiB
C#
using System.IO;
|
|
using BizHawk.Emulation.Cores.Nintendo.NES;
|
|
|
|
namespace BizHawk.Client.Common.movie.import
|
|
{
|
|
// FMV file format: http://tasvideos.org/FMV.html
|
|
// ReSharper disable once UnusedMember.Global
|
|
[ImporterFor("Famtasia", ".fmv")]
|
|
internal class FmvImport : MovieImporter
|
|
{
|
|
private IControllerDeck _deck;
|
|
|
|
protected override void RunImport()
|
|
{
|
|
using var fs = SourceFile.Open(FileMode.Open, FileAccess.Read);
|
|
using var r = new BinaryReader(fs);
|
|
// 000 4-byte signature: 46 4D 56 1A "FMV\x1A"
|
|
var signature = new string(r.ReadChars(4));
|
|
if (signature != "FMV\x1A")
|
|
{
|
|
Result.Errors.Add("This is not a valid .FMV file.");
|
|
return;
|
|
}
|
|
|
|
// 004 1-byte flags:
|
|
byte flags = r.ReadByte();
|
|
|
|
// bit 7: 0=reset-based, 1=savestate-based
|
|
if (((flags >> 2) & 0x1) != 0)
|
|
{
|
|
Result.Errors.Add("Movies that begin with a savestate are not supported.");
|
|
return;
|
|
}
|
|
|
|
Result.Movie.HeaderEntries[HeaderKeys.PLATFORM] = "NES";
|
|
var syncSettings = new NES.NESSyncSettings();
|
|
|
|
// other bits: unknown, set to 0
|
|
// 005 1-byte flags:
|
|
flags = r.ReadByte();
|
|
|
|
// bit 5: is a FDS recording
|
|
bool fds;
|
|
if (((flags >> 5) & 0x1) != 0)
|
|
{
|
|
fds = true;
|
|
Result.Movie.HeaderEntries[HeaderKeys.BOARDNAME] = "FDS";
|
|
}
|
|
else
|
|
{
|
|
fds = false;
|
|
}
|
|
|
|
// bit 6: uses controller 2
|
|
bool controller2 = ((flags >> 6) & 0x1) != 0;
|
|
|
|
// bit 7: uses controller 1
|
|
bool controller1 = ((flags >> 7) & 0x1) != 0;
|
|
|
|
// other bits: unknown, set to 0
|
|
// 006 4-byte little-endian unsigned int: unknown, set to 00000000
|
|
r.ReadInt32();
|
|
|
|
// 00A 4-byte little-endian unsigned int: rerecord count minus 1
|
|
uint rerecordCount = r.ReadUInt32();
|
|
|
|
/*
|
|
The rerecord count stored in the file is the number of times a savestate was loaded. If a savestate was never
|
|
loaded, the number is 0. Famtasia however displays "1" in such case. It always adds 1 to the number found in
|
|
the file.
|
|
*/
|
|
Result.Movie.Rerecords = rerecordCount + 1;
|
|
|
|
// 00E 2-byte little-endian unsigned int: unknown, set to 0000
|
|
r.ReadInt16();
|
|
|
|
// 010 64-byte zero-terminated emulator identifier string
|
|
string emuVersion = NullTerminated(new string(r.ReadChars(64)));
|
|
Result.Movie.Comments.Add($"{EmulationOrigin} Famtasia version {emuVersion}");
|
|
Result.Movie.Comments.Add($"{MovieOrigin} .FMV");
|
|
|
|
// 050 64-byte zero-terminated movie title string
|
|
string description = NullTerminated(new string(r.ReadChars(64)));
|
|
Result.Movie.Comments.Add(description);
|
|
if (!controller1 && !controller2 && !fds)
|
|
{
|
|
Result.Warnings.Add("No input recorded.");
|
|
}
|
|
|
|
var controllerSettings = new NESControlSettings
|
|
{
|
|
NesLeftPort = controller1 ? nameof(ControllerNES) : nameof(UnpluggedNES),
|
|
NesRightPort = controller2 ? nameof(ControllerNES) : nameof(UnpluggedNES)
|
|
};
|
|
_deck = controllerSettings.Instantiate((x, y) => true);
|
|
syncSettings.Controls.NesLeftPort = controllerSettings.NesLeftPort;
|
|
syncSettings.Controls.NesRightPort = controllerSettings.NesRightPort;
|
|
|
|
AddDeckControlButtons();
|
|
|
|
var controllers = new SimpleController
|
|
{
|
|
Definition = _deck.GetDefinition()
|
|
};
|
|
|
|
/*
|
|
* 01 Right
|
|
* 02 Left
|
|
* 04 Up
|
|
* 08 Down
|
|
* 10 B
|
|
* 20 A
|
|
* 40 Select
|
|
* 80 Start
|
|
*/
|
|
string[] buttons = { "Right", "Left", "Up", "Down", "B", "A", "Select", "Start" };
|
|
bool[] masks = { controller1, controller2, fds };
|
|
/*
|
|
The file has no terminator byte or frame count. The number of frames is the <filesize minus 144> divided by
|
|
<number of bytes per frame>.
|
|
*/
|
|
int bytesPerFrame = 0;
|
|
for (int player = 1; player <= masks.Length; player++)
|
|
{
|
|
if (masks[player - 1])
|
|
{
|
|
bytesPerFrame++;
|
|
}
|
|
}
|
|
|
|
long frameCount = (fs.Length - 144) / bytesPerFrame;
|
|
for (long frame = 1; frame <= frameCount; frame++)
|
|
{
|
|
/*
|
|
Each frame consists of 1 or more bytes. Controller 1 takes 1 byte, controller 2 takes 1 byte, and the FDS
|
|
data takes 1 byte. If all three exist, the frame is 3 bytes. For example, if the movie is a regular NES game
|
|
with only controller 1 data, a frame is 1 byte.
|
|
*/
|
|
for (int player = 1; player <= masks.Length; player++)
|
|
{
|
|
if (!masks[player - 1])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
byte controllerState = r.ReadByte();
|
|
if (player != 3)
|
|
{
|
|
for (int button = 0; button < buttons.Length; button++)
|
|
{
|
|
controllers[$"P{player} {buttons[button]}"] = ((controllerState >> button) & 0x1) != 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Result.Warnings.Add("FDS commands are not properly supported.");
|
|
}
|
|
}
|
|
|
|
Result.Movie.AppendFrame(controllers);
|
|
}
|
|
|
|
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(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");
|
|
}
|
|
|
|
|
|
}
|
|
}
|