BizHawk/BizHawk.Client.Common/movie/import/MmvImport.cs

150 lines
4.2 KiB
C#
Raw Normal View History

using System.IO;
using BizHawk.Common.BufferExtensions;
using BizHawk.Emulation.Cores.Sega.MasterSystem;
namespace BizHawk.Client.Common.movie.import
{
// ReSharper disable once UnusedMember.Global
// MMV file format: http://tasvideos.org/MMV.html
2019-11-15 00:01:16 +00:00
[ImporterFor("Dega", ".mmv")]
2019-11-14 23:19:59 +00:00
internal class MmvImport : MovieImporter
{
protected override void RunImport()
{
using var fs = SourceFile.Open(FileMode.Open, FileAccess.Read);
using var r = new BinaryReader(fs);
// 0000: 4-byte signature: "MMV\0"
string signature = new string(r.ReadChars(4));
if (signature != "MMV\0")
{
Result.Errors.Add("This is not a valid .MMV file.");
return;
}
// 0004: 4-byte little endian unsigned int: dega version
uint emuVersion = r.ReadUInt32();
Result.Movie.Comments.Add($"{MovieOrigin} .MMV");
Result.Movie.Comments.Add($"{EmulationOrigin} Dega version {emuVersion}");
// 0008: 4-byte little endian unsigned int: frame count
uint frameCount = r.ReadUInt32();
// 000c: 4-byte little endian unsigned int: rerecord count
uint rerecordCount = r.ReadUInt32();
Result.Movie.Rerecords = rerecordCount;
// 0010: 4-byte little endian flag: begin from reset?
uint reset = r.ReadUInt32();
if (reset == 0)
{
Result.Errors.Add("Movies that begin with a savestate are not supported.");
return;
}
// 0014: 4-byte little endian unsigned int: offset of state information
r.ReadUInt32();
// 0018: 4-byte little endian unsigned int: offset of input data
r.ReadUInt32();
// 001c: 4-byte little endian unsigned int: size of input packet
r.ReadUInt32();
// 0020-005f: string: author info (UTF-8)
string author = NullTerminated(new string(r.ReadChars(64)));
Result.Movie.HeaderEntries[HeaderKeys.AUTHOR] = author;
// 0060: 4-byte little endian flags
byte flags = r.ReadByte();
// bit 0: unused
// bit 1: "PAL"
bool pal = ((flags >> 1) & 0x1) != 0;
Result.Movie.HeaderEntries[HeaderKeys.PAL] = pal.ToString();
// bit 2: Japan
bool japan = ((flags >> 2) & 0x1) != 0;
Result.Movie.HeaderEntries["Japan"] = japan.ToString();
// bit 3: Game Gear (version 1.16+)
bool isGameGear;
if (((flags >> 3) & 0x1) != 0)
{
isGameGear = true;
Result.Movie.HeaderEntries.Add("IsGGMode", "1");
}
else
{
isGameGear = false;
}
Result.Movie.HeaderEntries[HeaderKeys.PLATFORM] = "SMS"; // System Id is still SMS even if game gear
// bits 4-31: unused
r.ReadBytes(3);
// 0064-00e3: string: rom name (ASCII)
string gameName = NullTerminated(new string(r.ReadChars(128)));
Result.Movie.HeaderEntries[HeaderKeys.GAMENAME] = gameName;
// 00e4-00f3: binary: rom MD5 digest
byte[] md5 = r.ReadBytes(16);
Result.Movie.HeaderEntries[MD5] = md5.BytesToHexString().ToLower();
var ss = new SMS.SmsSyncSettings
{
ControllerType = SMS.SmsSyncSettings.ControllerTypes.Standard
};
var controllers = new SimpleController
{
Definition = isGameGear
? SMS.SmsController
: SMS.GGController
};
/*
76543210
* bit 0 (0x01): up
* bit 1 (0x02): down
* bit 2 (0x04): left
* bit 3 (0x08): right
* bit 4 (0x10): 1
* bit 5 (0x20): 2
* bit 6 (0x40): start (Master System)
* bit 7 (0x80): start (Game Gear)
*/
string[] buttons = { "Up", "Down", "Left", "Right", "B1", "B2" };
for (int frame = 1; frame <= frameCount; frame++)
{
/*
Controller data is made up of one input packet per frame. Each packet currently consists of 2 bytes. The
first byte is for controller 1 and the second controller 2. The Game Gear only uses the controller 1 input
however both bytes are still present.
*/
for (int player = 1; player <= 2; player++)
{
byte controllerState = r.ReadByte();
for (int button = 0; button < buttons.Length; button++)
{
controllers[$"P{player} {buttons[button]}"] = ((controllerState >> button) & 0x1) != 0;
}
if (player == 1)
{
controllers["Pause"] =
(((controllerState >> 6) & 0x1) != 0 && (!isGameGear))
|| (((controllerState >> 7) & 0x1) != 0 && isGameGear);
}
}
Result.Movie.AppendFrame(controllers);
}
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss);
}
}
}