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

315 lines
10 KiB
C#

using System;
using System.IO;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Nintendo.GBA;
using BizHawk.Emulation.Cores.Nintendo.GBHawk;
using BizHawk.Emulation.Cores.Nintendo.Gameboy;
namespace BizHawk.Client.Common.movie.import
{
// VBM file format: http://code.google.com/p/vba-rerecording/wiki/VBM
// ReSharper disable once UnusedMember.Global
[ImporterFor("Visual Boy Advance", ".vbm")]
internal class VbmImport : MovieImporter
{
protected override void RunImport()
{
using var fs = SourceFile.Open(FileMode.Open, FileAccess.Read);
using var r = new BinaryReader(fs);
bool is_GBC = false;
// 000 4-byte signature: 56 42 4D 1A "VBM\x1A"
string signature = new string(r.ReadChars(4));
if (signature != "VBM\x1A")
{
Result.Errors.Add("This is not a valid .VBM file.");
return;
}
// 004 4-byte little-endian unsigned int: major version number, must be "1"
uint majorVersion = r.ReadUInt32();
if (majorVersion != 1)
{
Result.Errors.Add(".VBM major movie version must be 1.");
return;
}
/*
008 4-byte little-endian integer: movie "uid" - identifies the movie-savestate relationship, also used as the
recording time in Unix epoch format
*/
uint uid = r.ReadUInt32();
// 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;
// 014 1-byte flags: (movie start flags)
byte flags = r.ReadByte();
// bit 0: if "1", movie starts from an embedded "quicksave" snapshot
bool startFromSavestate = (flags & 0x1) != 0;
// bit 1: if "1", movie starts from reset with an embedded SRAM
bool startFromSram = ((flags >> 1) & 0x1) != 0;
// other: reserved, set to 0
// (If both bits 0 and 1 are "1", the movie file is invalid)
if (startFromSavestate && startFromSram)
{
Result.Errors.Add("This is not a valid .VBM file.");
return;
}
if (startFromSavestate)
{
Result.Errors.Add("Movies that begin with a savestate are not supported.");
return;
}
if (startFromSram)
{
Result.Errors.Add("Movies that begin with SRAM are not supported.");
return;
}
// 015 1-byte flags: controller flags
byte controllerFlags = r.ReadByte();
/*
* bit 0: controller 1 in use
* bit 1: controller 2 in use (SGB games can be 2-player multiplayer)
* bit 2: controller 3 in use (SGB games can be 3- or 4-player multiplayer with multitap)
* bit 3: controller 4 in use (SGB games can be 3- or 4-player multiplayer with multitap)
*/
bool[] controllersUsed = new bool[4];
for (int controller = 1; controller <= controllersUsed.Length; controller++)
{
controllersUsed[controller - 1] = ((controllerFlags >> (controller - 1)) & 0x1) != 0;
}
if (!controllersUsed[0])
{
Result.Errors.Add("Controller 1 must be in use.");
return;
}
// other: reserved
// 016 1-byte flags: system flags (game always runs at 60 frames/sec)
flags = r.ReadByte();
// bit 0: if "1", movie is for the GBA system
bool isGBA = (flags & 0x1) != 0;
// bit 1: if "1", movie is for the GBC system
bool isGBC = ((flags >> 1) & 0x1) != 0;
// bit 2: if "1", movie is for the SGB system
bool isSGB = ((flags >> 2) & 0x1) != 0;
// (If all 3 of these bits are "0", it is for regular GB.)
string platform = "GB";
if (isGBA)
{
platform = "GBA";
var mGBAName = ((CoreAttribute)Attribute.GetCustomAttribute(typeof(MGBAHawk), typeof(CoreAttribute))).CoreName;
Result.Movie.HeaderEntries[HeaderKeys.Core] = mGBAName;
}
if (isGBC)
{
is_GBC = true;
platform = "GB";
Result.Movie.HeaderEntries.Add("IsCGBMode", "1");
}
if (isSGB)
{
Result.Errors.Add("SGB imports are not currently supported");
}
Result.Movie.HeaderEntries[HeaderKeys.Platform] = platform;
// 017 1-byte flags: (values of some boolean emulator options)
flags = r.ReadByte();
/*
* bit 0: (useBiosFile) if "1" and the movie is of a GBA game, the movie was made using a GBA BIOS file.
* bit 1: (skipBiosFile) if "0" and the movie was made with a GBA BIOS file, the BIOS intro is included in the
* movie.
* bit 2: (rtcEnable) if "1", the emulator "real time clock" feature was enabled.
* bit 3: (unsupported) must be "0" or the movie file is considered invalid (legacy).
*/
if (((flags >> 3) & 0x1) != 0)
{
Result.Errors.Add("This is not a valid .VBM file.");
return;
}
/*
018 4-byte little-endian unsigned int: theApp.winSaveType (value of that emulator option)
01C 4-byte little-endian unsigned int: theApp.winFlashSize (value of that emulator option)
020 4-byte little-endian unsigned int: gbEmulatorType (value of that emulator option)
*/
r.ReadBytes(12);
/*
024 12-byte character array: the internal game title of the ROM used while recording, not necessarily
null-terminated (ASCII?)
*/
string gameName = NullTerminated(new string(r.ReadChars(12)));
Result.Movie.HeaderEntries[HeaderKeys.GameName] = gameName;
// 030 1-byte unsigned char: minor version/revision number of current VBM version, the latest is "1"
byte minorVersion = r.ReadByte();
Result.Movie.Comments.Add($"{MovieOrigin} .VBM version {majorVersion}.{minorVersion}");
Result.Movie.Comments.Add($"{EmulationOrigin} Visual Boy Advance");
// 031 1-byte unsigned char: the internal CRC of the ROM used while recording
r.ReadByte();
/*
032 2-byte little-endian unsigned short: the internal Checksum of the ROM used while recording, or a
calculated CRC16 of the BIOS if GBA
*/
ushort checksumCRC16 = r.ReadUInt16();
/*
034 4-byte little-endian unsigned int: the Game Code of the ROM used while recording, or the Unit Code if not
GBA
*/
uint gameCodeUnitCode = r.ReadUInt32();
if (platform == "GBA")
{
Result.Movie.HeaderEntries["CRC16"] = checksumCRC16.ToString();
Result.Movie.HeaderEntries["GameCode"] = gameCodeUnitCode.ToString();
}
else
{
Result.Movie.HeaderEntries["InternalChecksum"] = checksumCRC16.ToString();
Result.Movie.HeaderEntries["UnitCode"] = gameCodeUnitCode.ToString();
}
// 038 4-byte little-endian unsigned int: offset to the savestate or SRAM inside file, set to 0 if unused
r.ReadBytes(4);
// 03C 4-byte little-endian unsigned int: offset to the controller data inside file
uint firstFrameOffset = r.ReadUInt32();
// After the header is 192 bytes of text. The first 64 of these 192 bytes are for the author's name (or names).
string author = NullTerminated(new string(r.ReadChars(64)));
Result.Movie.HeaderEntries[HeaderKeys.Author] = author;
// The following 128 bytes are for a description of the movie. Both parts must be null-terminated.
string movieDescription = NullTerminated(new string(r.ReadChars(128)));
Result.Movie.Comments.Add(movieDescription);
r.BaseStream.Position = firstFrameOffset;
SimpleController controllers = isGBA
? GbaController()
: GbController();
/*
* 01 00 A
* 02 00 B
* 04 00 Select
* 08 00 Start
* 10 00 Right
* 20 00 Left
* 40 00 Up
* 80 00 Down
* 00 01 R
* 00 02 L
*/
string[] buttons = { "A", "B", "Select", "Start", "Right", "Left", "Up", "Down", "R", "L" };
/*
* 00 04 Reset (old timing)
* 00 08 Reset (new timing since version 1.1)
* 00 10 Left motion sensor
* 00 20 Right motion sensor
* 00 40 Down motion sensor
* 00 80 Up motion sensor
*/
string[] other =
{
"Reset (old timing)", "Reset (new timing since version 1.1)", "Left motion sensor",
"Right motion sensor", "Down motion sensor", "Up motion sensor"
};
for (int frame = 1; frame <= frameCount; frame++)
{
/*
A stream of 2-byte bitvectors which indicate which buttons are pressed at each point in time. They will
come in groups of however many controllers are active, in increasing order.
*/
ushort controllerState = r.ReadUInt16();
for (int button = 0; button < buttons.Length; button++)
{
controllers[buttons[button]] = ((controllerState >> button) & 0x1) != 0;
if (((controllerState >> button) & 0x1) != 0 && button > 7)
{
continue;
}
}
// TODO: Handle the other buttons.
for (int button = 0; button < other.Length; button++)
{
if (((controllerState >> (button + 10)) & 0x1) != 0)
{
Result.Warnings.Add($"Unable to import {other[button]} at frame {frame}.");
break;
}
}
// TODO: Handle the additional controllers.
for (int player = 2; player <= controllersUsed.Length; player++)
{
if (controllersUsed[player - 1])
{
r.ReadBytes(2);
}
}
Result.Movie.AppendFrame(controllers);
}
if (isGBA)
{
Global.Config.GbaUsemGba = true;
var ss = new MGBAHawk.SyncSettings { SkipBios = true };
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss);
}
else
{
if (Global.Config.GbUseGbHawk || Global.Config.UseSubGBHawk)
{
var temp_sync = new GBHawk.GBSyncSettings();
if (is_GBC) { temp_sync.ConsoleMode = GBHawk.GBSyncSettings.ConsoleModeType.GBC; }
else { temp_sync.ConsoleMode = GBHawk.GBSyncSettings.ConsoleModeType.GB; }
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(temp_sync);
}
else
{
var temp_sync = new Gameboy.GambatteSyncSettings();
if (is_GBC) { temp_sync.ConsoleMode = Gameboy.GambatteSyncSettings.ConsoleModeType.GBC; }
else { temp_sync.ConsoleMode = Gameboy.GambatteSyncSettings.ConsoleModeType.GB; }
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(temp_sync);
}
}
}
private static SimpleController GbController()
{
return new SimpleController
{
Definition = new ControllerDefinition
{
BoolButtons = { "Up", "Down", "Left", "Right", "Start", "Select", "B", "A", "Power" }
}
};
}
private static SimpleController GbaController()
=> new SimpleController { Definition = GBA.GBAController };
}
}