using System; using System.IO; using BizHawk.Emulation.Common; using BizHawk.Emulation.Cores.Nintendo.GBA; using BizHawk.Emulation.Cores.Nintendo.GBHawk; 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); // 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) { platform = "GBC"; } 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 { Global.Config.GbUseGbHawk = true; Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(new GBHawk.GBSyncSettings()); } } private SimpleController GbController() { return new SimpleController { Definition = new ControllerDefinition { BoolButtons = { "Up", "Down", "Left", "Right", "Start", "Select", "B", "A", "Power" } } }; } private SimpleController GbaController() { return new SimpleController { Definition = GBA.GBAController }; } } }