using System.IO; using BizHawk.Emulation.Cores.Consoles.Sega.gpgx; namespace BizHawk.Client.Common.movie.import { // GMV file format: http://code.google.com/p/gens-rerecording/wiki/GMV // ReSharper disable once UnusedMember.Global [ImporterFor("GENS", ".gmv")] internal class GmvImport : MovieImporter { protected override void RunImport() { using var fs = SourceFile.Open(FileMode.Open, FileAccess.Read); using var r = new BinaryReader(fs); // 000 16-byte signature and format version: "Gens Movie TEST9" string signature = new string(r.ReadChars(15)); if (signature != "Gens Movie TEST") { Result.Errors.Add("This is not a valid .GMV file."); return; } Result.Movie.HeaderEntries[HeaderKeys.PLATFORM] = "GEN"; // 00F ASCII-encoded GMV file format version. The most recent is 'A'. (?) string version = new string(r.ReadChars(1)); Result.Movie.Comments.Add($"{MovieOrigin} .GMV version {version}"); Result.Movie.Comments.Add($"{EmulationOrigin} Gens"); // 010 4-byte little-endian unsigned int: rerecord count uint rerecordCount = r.ReadUInt32(); Result.Movie.Rerecords = rerecordCount; // 014 ASCII-encoded controller config for player 1. '3' or '6'. char player1Config = r.ReadChar(); // 015 ASCII-encoded controller config for player 2. '3' or '6'. char player2Config = r.ReadChar(); // 016 special flags (Version A and up only) byte flags = r.ReadByte(); /* bit 7 (most significant): if "1", movie runs at 50 frames per second; if "0", movie runs at 60 frames per second The file format has no means of identifying NTSC/"PAL", but the FPS can still be derived from the header. */ bool pal = ((flags >> 7) & 0x1) != 0; Result.Movie.HeaderEntries[HeaderKeys.PAL] = pal.ToString(); // bit 6: if "1", movie requires a savestate. if (((flags >> 6) & 0x1) != 0) { Result.Errors.Add("Movies that begin with a savestate are not supported."); return; } // bit 5: if "1", movie is 3-player movie; if "0", movie is 2-player movie bool threePlayers = ((flags >> 5) & 0x1) != 0; LibGPGX.InputData input = new LibGPGX.InputData(); input.dev[0] = player1Config == '6' ? LibGPGX.INPUT_DEVICE.DEVICE_PAD6B : LibGPGX.INPUT_DEVICE.DEVICE_PAD3B; input.dev[1] = player2Config == '6' ? LibGPGX.INPUT_DEVICE.DEVICE_PAD6B : LibGPGX.INPUT_DEVICE.DEVICE_PAD3B; var ss = new GPGX.GPGXSyncSettings { UseSixButton = player1Config == '6' || player2Config == '6', ControlType = GPGX.ControlType.Normal }; input.dev[2] = input.dev[3] = input.dev[4] = input.dev[5] = input.dev[6] = input.dev[7] = LibGPGX.INPUT_DEVICE.DEVICE_NONE; if (threePlayers) { input.dev[2] = ss.UseSixButton ? LibGPGX.INPUT_DEVICE.DEVICE_PAD6B : LibGPGX.INPUT_DEVICE.DEVICE_PAD3B; } var controlConverter = new GPGXControlConverter(input, false); var controller = new SimpleController { Definition = controlConverter.ControllerDef }; // Unknown. r.ReadByte(); // 018 40-byte zero-terminated ASCII movie name string string description = NullTerminated(new string(r.ReadChars(40))); Result.Movie.Comments.Add(description); /* 040 frame data For controller bytes, each value is determined by OR-ing together values for whichever of the following are left unpressed: * 0x01 Up * 0x02 Down * 0x04 Left * 0x08 Right * 0x10 A * 0x20 B * 0x40 C * 0x80 Start */ string[] buttons = { "Up", "Down", "Left", "Right", "A", "B", "C", "Start" }; /* For XYZ-mode, each value is determined by OR-ing together values for whichever of the following are left unpressed: * 0x01 Controller 1 X * 0x02 Controller 1 Y * 0x04 Controller 1 Z * 0x08 Controller 1 Mode * 0x10 Controller 2 X * 0x20 Controller 2 Y * 0x40 Controller 2 Z * 0x80 Controller 2 Mode */ string[] other = { "X", "Y", "Z", "Mode" }; // The file has no terminator byte or frame count. The number of frames is the divided by 3. long frameCount = (fs.Length - 64) / 3; for (long frame = 1; frame <= frameCount; frame++) { // Each frame consists of 3 bytes. for (int player = 1; player <= 3; player++) { byte controllerState = r.ReadByte(); // * is controller 3 if a 3-player movie, or XYZ-mode if a 2-player movie. if (player != 3 || threePlayers) { for (int button = 0; button < buttons.Length; button++) { controller[$"P{player} {buttons[button]}"] = ((controllerState >> button) & 0x1) == 0; } } else { for (int button = 0; button < other.Length; button++) { if (player1Config == '6') { controller[$"P1 {other[button]}"] = ((controllerState >> button) & 0x1) == 0; } if (player2Config == '6') { controller[$"P2 {other[button]}"] = ((controllerState >> (button + 4)) & 0x1) == 0; } } } } Result.Movie.AppendFrame(controller); } Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss); } } }