public void PrepareHapticsForHost(SimpleController finalHostController) { foreach (var kvp in _feedbackBindings) { if (_haptics.TryGetValue(kvp.Key, out var strength)) { finalHostController.SetHapticChannelStrength(kvp.Value.GamepadPrefix + kvp.Value.Channel, (int)((double)strength * kvp.Value.Prescale)); } } }
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"); }
private void ImportInputFrame(string line) { var controllers = new SimpleController { Definition = _deck.GetDefinition() }; string[] sections = line.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); controllers["Reset"] = sections[1][0] == '1'; switch (sections[0][0]) { case '0': break; case '1': controllers["Reset"] = true; break; case '2': controllers["Power"] = true; break; case '4': controllers["FDS Insert 0"] = true; break; case '8': controllers["FDS Insert 1"] = true; break; // TODO: insert coin? default: Result.Warnings.Add($"Unknown command: {sections[0][0]}"); break; } for (int player = 1; player < sections.Length; player++) { string prefix = $"P{player} "; // Only count lines with that have the right number of buttons and are for valid players. if (sections[player].Length == _buttons.Length) { for (int button = 0; button < _buttons.Length; button++) { // Consider the button pressed so long as its spot is not occupied by a ".". controllers[prefix + _buttons[button]] = sections[player][button] != '.'; } } } Result.Movie.AppendFrame(controllers); }
public void PrepareHapticsForHost(SimpleController finalHostController) { foreach (var(k, v) in _feedbackBindings) { if (_haptics.TryGetValue(k, out var strength)) { foreach (var hostChannel in v.Channels !.Split('+')) { finalHostController.SetHapticChannelStrength(v.GamepadPrefix + hostChannel, (int)((double)strength * v.Prescale)); } } } }
private void ImportInputFrame(string line) { var controller = new SimpleController { Definition = new ControllerDefinition { BoolButtons = { "Up", "Down", "Left", "Right", "Start", "Select", "B", "A", "Y", "X", "L", "R", "LidOpen", "LidClose", "Touch", "Power" } }.AddXYPair("Touch {0}", AxisPairOrientation.RightAndUp, 0.RangeTo(255), 128, 0.RangeTo(191), 96) //TODO verify direction against hardware .AddAxis("Mic Input", 0.RangeTo(2047), 0) .AddAxis("GBA Light Sensor", 0.RangeTo(10), 0) }; controller["LidOpen"] = false; controller["LidClose"] = false; controller["Power"] = false; string[] sections = line.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); if (sections.Length > 0) { ProcessCmd(sections[0], controller); } if (sections.Length > 1) { var mnemonics = sections[1].Take(_buttons.Length).ToList(); controller["Left"] = mnemonics[1] != '.'; controller["Right"] = mnemonics[0] != '.'; controller["Up"] = mnemonics[3] != '.'; controller["Down"] = mnemonics[2] != '.'; controller["A"] = mnemonics[7] != '.'; controller["B"] = mnemonics[6] != '.'; controller["X"] = mnemonics[9] != '.'; controller["Y"] = mnemonics[8] != '.'; controller["L"] = mnemonics[10] != '.'; controller["R"] = mnemonics[11] != '.'; controller["Start"] = mnemonics[4] != '.'; controller["Select"] = mnemonics[5] != '.'; controller["Touch"] = sections[1].Substring(21, 1) != "0"; var touchX = int.Parse(sections[1].Substring(13, 3)); var touchY = int.Parse(sections[1].Substring(17, 3)); controller.AcceptNewAxes(new[]
private void ImportInputFrame(string line) { var controller = new SimpleController { Definition = new ControllerDefinition { BoolButtons = { "Right", "Left", "Down", "Up", "Start", "Select", "B", "A", "X", "Y", "L", "R", "LidOpen","LidClose", "Touch" } } }; controller.Definition.AxisControls.Add("TouchX"); controller.Definition.AxisRanges.Add(new ControllerDefinition.AxisRange(0, 128, 255)); controller.Definition.AxisControls.Add("TouchY"); controller.Definition.AxisRanges.Add(new ControllerDefinition.AxisRange(0, 96, 191)); controller["LidOpen"] = false; controller["LidOpen"] = false; string[] sections = line.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); if (sections.Length > 0) { ProcessCmd(sections[0], controller); } if (sections.Length > 1) { var mnemonics = sections[1].Take(_buttons.Length).ToList(); for (var i = 0; i < mnemonics.Count; i++) { controller[_buttons[i]] = mnemonics[i] != '.'; } controller["Touch"] = sections[1].Substring(21, 1) != "0"; var touchX = int.Parse(sections[1].Substring(13, 3)); var touchY = int.Parse(sections[1].Substring(17, 3)); controller.AcceptNewAxes(new[]
private void ImportInputFrame(string line) { var controller = new SimpleController { Definition = new ControllerDefinition { BoolButtons = { "Right", "Left", "Down", "Up", "Start", "Select", "B", "A", "X", "Y", "L", "R", "LidOpen","LidClose", "Touch" } }.AddXYPair("Touch{0}", AxisPairOrientation.RightAndUp, 0.RangeTo(255), 128, 0.RangeTo(191), 96) //TODO verify direction against hardware }; controller["LidOpen"] = false; controller["LidOpen"] = false; string[] sections = line.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); if (sections.Length > 0) { ProcessCmd(sections[0], controller); } if (sections.Length > 1) { var mnemonics = sections[1].Take(_buttons.Length).ToList(); for (var i = 0; i < mnemonics.Count; i++) { controller[_buttons[i]] = mnemonics[i] != '.'; } controller["Touch"] = sections[1].Substring(21, 1) != "0"; var touchX = int.Parse(sections[1].Substring(13, 3)); var touchY = int.Parse(sections[1].Substring(17, 3)); controller.AcceptNewAxes(new[]
// Import a frame from a text-based format. private static BkmMovie ImportTextFrame(string line, int lineNum, BkmMovie m, string path, string platform, ref string warningMsg) { string[] buttons = { }; var controller = string.Empty; var ext = path != null ? Path.GetExtension(path).ToUpper() : ""; switch (ext) { case ".FM2": buttons = new[] { "Right", "Left", "Down", "Up", "Start", "Select", "B", "A" }; controller = "NES Controller"; break; case ".MC2": buttons = new[] { "Up", "Down", "Left", "Right", "B1", "B2", "Run", "Select" }; controller = "PC Engine Controller"; break; case ".LSMV": buttons = new[] { "B", "Y", "Select", "Start", "Up", "Down", "Left", "Right", "A", "X", "L", "R" }; controller = "SNES Controller"; if (platform == "GB" || platform == "GBC") { buttons = new[] { "A", "B", "Select", "Start", "Right", "Left", "Up", "Down" }; controller = "Gameboy Controller"; } break; case ".YMV": buttons = new[] { "Left", "Right", "Up", "Down", "Start", "A", "B", "C", "X", "Y", "Z", "L", "R" }; controller = "Saturn Controller"; break; } var controllers = new SimpleController { Type = new ControllerDefinition { Name = controller } }; // Split up the sections of the frame. string[] sections = line.Split('|'); if (ext == ".FM2" && sections.Length >= 2 && sections[1].Length != 0) { controllers["Reset"] = (sections[1][0] == '1'); // Get the first invalid command warning message that arises. if (string.IsNullOrEmpty((warningMsg))) { switch (sections[1][0]) { case '0': break; case '1': break; case '2': if (m.FrameCount != 0) { warningMsg = "hard reset"; } break; case '4': warningMsg = "FDS Insert"; break; case '8': warningMsg = "FDS Select Side"; break; default: warningMsg = "unknown"; break; } if (warningMsg != "") { warningMsg = "Unable to import " + warningMsg + " command on line " + lineNum + "."; } } } if (ext == ".LSMV" && sections.Length != 0) { string flags = sections[0]; char[] off = { '.', ' ', '\t', '\n', '\r' }; if (flags.Length == 0 || off.Contains(flags[0])) { if (warningMsg == "") warningMsg = "Unable to import subframe."; return m; } bool reset = (flags.Length >= 2 && !off.Contains(flags[1])); flags = SingleSpaces(flags.Substring(2)); if (reset && ((flags.Length >= 2 && flags[1] != '0') || (flags.Length >= 4 && flags[3] != '0'))) { if (warningMsg == "") { warningMsg = "Unable to import delayed reset."; } return m; } controllers["Reset"] = reset; } /* Skip the first two sections of the split, which consist of everything before the starting | and the command. Do not use the section after the last |. In other words, get the sections for the players. */ int start = 2; int end = sections.Length - 1; int player_offset = -1; if (ext == ".LSMV") { // LSNES frames don't start or end with a |. start--; end++; player_offset++; } for (int section = start; section < end; section++) { // The player number is one less than the section number for the reasons explained above. int player = section + player_offset; string prefix = "P" + (player) + " "; // Gameboy doesn't currently have a prefix saying which player the input is for. if (controllers.Type.Name == "Gameboy Controller") { prefix = ""; } // Only count lines with that have the right number of buttons and are for valid players. if ( sections[section].Length == buttons.Length && player <= BkmMnemonicConstants.PLAYERS[controllers.Type.Name] ) { for (int button = 0; button < buttons.Length; button++) { // Consider the button pressed so long as its spot is not occupied by a ".". controllers[prefix + buttons[button]] = (sections[section][button] != '.'); } } } // Convert the data for the controllers to a mnemonic and add it as a frame. m.AppendFrame(controllers); return m; }
// VMV file format: http://tasvideos.org/VMV.html private static BkmMovie ImportVMV(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 000 12-byte signature: "VirtuaNES MV" string signature = r.ReadStringFixedAscii(12); if (signature != "VirtuaNES MV") { errorMsg = "This is not a valid .VMV file."; r.Close(); fs.Close(); return null; } m.Header[HeaderKeys.PLATFORM] = "NES"; // 00C 2-byte little-endian integer: movie version 0x0400 ushort version = r.ReadUInt16(); m.Comments.Add(MOVIEORIGIN + " .VMV version " + version); m.Comments.Add(EMULATIONORIGIN + " VirtuaNES"); // 00E 2-byte little-endian integer: record version ushort recordVersion = r.ReadUInt16(); m.Comments.Add(COMMENT + " Record version " + recordVersion); // 010 4-byte flags (control byte) uint flags = r.ReadUInt32(); /* * bit 0: controller 1 in use * bit 1: controller 2 in use * bit 2: controller 3 in use * bit 3: controller 4 in use */ bool[] controllersUsed = new bool[4]; for (int controller = 1; controller <= controllersUsed.Length; controller++) { controllersUsed[controller - 1] = (((flags >> (controller - 1)) & 0x1) != 0); } bool fourscore = (controllersUsed[2] || controllersUsed[3]); m.Header[HeaderKeys.FOURSCORE] = fourscore.ToString(); /* bit 6: 1=reset-based, 0=savestate-based (movie version <= 0x300 is always savestate-based) If the movie version is < 0x400, or the "from-reset" flag is not set, a savestate is loaded from the movie. Otherwise, the savestate is ignored. */ if (version < 0x400 || ((flags >> 6) & 0x1) == 0) { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } /* bit 7: disable rerecording For the other control bytes, if a key from 1P to 4P (whichever one) is entirely ON, the following 4 bytes becomes the controller data. TODO: Figure out what this means. Other bits: reserved, set to 0 */ // 014 DWORD Ext0; // ROM:program CRC FDS:program ID r.ReadBytes(4); // 018 WORD Ext1; // ROM:unused,0 FDS:maker ID r.ReadBytes(2); // 01A WORD Ext2; // ROM:unused,0 FDS:disk no. r.ReadBytes(2); // 01C 4-byte little-endian integer: rerecord count uint rerecordCount = r.ReadUInt32(); m.Rerecords = rerecordCount; /* 020 BYTE RenderMethod 0=POST_ALL,1=PRE_ALL 2=POST_RENDER,3=PRE_RENDER 4=TILE_RENDER */ r.ReadByte(); // 021 BYTE IRQtype // IRQ type r.ReadByte(); // 022 BYTE FrameIRQ // FrameIRQ not allowed r.ReadByte(); // 023 1-byte flag: 0=NTSC (60 Hz), 1="PAL" (50 Hz) bool pal = (r.ReadByte() == 1); m.Header[HeaderKeys.PAL] = pal.ToString(); // 024 8-bytes: reserved, set to 0 r.ReadBytes(8); // 02C 4-byte little-endian integer: save state start offset r.ReadBytes(4); // 030 4-byte little-endian integer: save state end offset r.ReadBytes(4); // 034 4-byte little-endian integer: movie data offset uint firstFrameOffset = r.ReadUInt32(); // 038 4-byte little-endian integer: movie frame count uint frameCount = r.ReadUInt32(); // 03C 4-byte little-endian integer: CRC (CRC excluding this data(to prevent cheating)) int crc32 = r.ReadInt32(); m.Header[CRC32] = crc32.ToString(); if (!controllersUsed[0] && !controllersUsed[1] && !controllersUsed[2] && !controllersUsed[3]) { warningMsg = "No input recorded."; r.Close(); fs.Close(); return m; } r.BaseStream.Position = firstFrameOffset; SimpleController controllers = new SimpleController { Type = new ControllerDefinition { Name = "NES Controller" } }; /* * 01 A * 02 B * 04 Select * 08 Start * 10 Up * 20 Down * 40 Left * 80 Right */ string[] buttons = new[] { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" }; for (int frame = 1; frame <= frameCount; frame++) { /* Each frame consists of 1 or more bytes. Controller 1 takes 1 byte, controller 2 takes 1 byte, controller 3 takes 1 byte, and controller 4 takes 1 byte. If all four exist, the frame is 4 bytes. For example, if the movie only has controller 1 data, a frame is 1 byte. */ controllers["Reset"] = false; for (int player = 1; player <= controllersUsed.Length; player++) { if (!controllersUsed[player - 1]) { continue; } byte controllerState = r.ReadByte(); if (controllerState >= 0xF0) { if (controllerState == 0xF0) { ushort command = r.ReadUInt16(); string commandName = ""; if ((command & 0xFF00) == 0) { switch (command & 0x00FF) { // NESCMD_NONE case 0: break; // NESCMD_HWRESET case 1: controllers["Reset"] = true; break; // NESCMD_SWRESET case 2: controllers["Reset"] = true; break; // NESCMD_EXCONTROLLER case 3: commandName = "NESCMD_EXCONTROLLER, 0"; break; // NESCMD_DISK_THROTTLE_ON case 4: commandName = "NESCMD_DISK_THROTTLE_ON, 0"; break; // NESCMD_DISK_THROTTLE_OFF case 5: commandName = "NESCMD_DISK_THROTTLE_OFF, 0"; break; // NESCMD_DISK_EJECT case 6: commandName = "NESCMD_DISK_EJECT, 0"; break; // NESCMD_DISK_0A case 7: commandName = "NESCMD_DISK_0A, 0"; break; // NESCMD_DISK_0B case 8: commandName = "NESCMD_DISK_0B, 0"; break; // NESCMD_DISK_1A case 9: commandName = "NESCMD_DISK_1A, 0"; break; // NESCMD_DISK_1B case 10: commandName = "NESCMD_DISK_1B, 0"; break; default: commandName = "invalid command"; break; } } else { commandName = "NESCMD_EXCONTROLLER, " + (command & 0xFF00); } if (commandName != "" && warningMsg == "") { warningMsg = "Unable to run command \"" + commandName + "\"."; } } else if (controllerState == 0xF3) { uint dwdata = r.ReadUInt32(); // TODO: Make a clearer warning message. if (warningMsg == "") { warningMsg = "Unable to run SetSyncExData(" + dwdata + ")."; } } controllerState = r.ReadByte(); } for (int button = 0; button < buttons.Length; button++) { controllers["P" + player + " " + buttons[button]] = (((controllerState >> button) & 0x1) != 0); } } m.AppendFrame(controllers); } r.Close(); fs.Close(); return m; }
// VBM file format: http://code.google.com/p/vba-rerecording/wiki/VBM private static BkmMovie ImportVBM(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 000 4-byte signature: 56 42 4D 1A "VBM\x1A" string signature = r.ReadStringFixedAscii(4); if (signature != "VBM\x1A") { errorMsg = "This is not a valid .VBM file."; r.Close(); fs.Close(); return null; } // 004 4-byte little-endian unsigned int: major version number, must be "1" uint majorVersion = r.ReadUInt32(); if (majorVersion != 1) { errorMsg = ".VBM major movie version must be 1."; r.Close(); fs.Close(); return null; } /* 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(); m.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 startfromquicksave = ((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 (startfromquicksave && startfromsram) { errorMsg = "This is not a valid .VBM file."; r.Close(); fs.Close(); return null; } if (startfromquicksave) { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } if (startfromsram) { errorMsg = "Movies that begin with SRAM are not supported."; r.Close(); fs.Close(); return null; } // 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]) { errorMsg = "Controller 1 must be in use."; r.Close(); fs.Close(); return null; } // 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 is_gba = ((flags & 0x1) != 0); // bit 1: if "1", movie is for the GBC system bool is_gbc = (((flags >> 1) & 0x1) != 0); // bit 2: if "1", movie is for the SGB system bool is_sgb = (((flags >> 2) & 0x1) != 0); // other: reserved, set to 0 // (At most one of bits 0, 1, 2 can be "1") //if (!(is_gba ^ is_gbc ^ is_sgb) && (is_gba || is_gbc || is_sgb)) //TODO: adelikat: this doesn't do what the comment above suggests it is trying to check for, it is always false! //{ //errorMsg = "This is not a valid .VBM file."; //r.Close(); //fs.Close(); //return null; //} // (If all 3 of these bits are "0", it is for regular GB.) string platform = "GB"; if (is_gba) { platform = "GBA"; } if (is_gbc) { platform = "GBC"; } if (is_sgb) { m.Comments.Add(SUPERGAMEBOYMODE + " True"); } m.Header[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) { errorMsg = "This is not a valid .VBM file."; r.Close(); fs.Close(); return null; } /* * bit 4: (lagReduction) if "0" and the movie is of a GBA game, the movie was made using the old excessively * laggy GBA timing. * bit 5: (gbcHdma5Fix) if "0" and the movie is of a GBC game, the movie was made using the old buggy HDMA5 * timing. * bit 6: (echoRAMFix) if "1" and the movie is of a GB, GBC, or SGB game, the movie was made with Echo RAM * Fix on, otherwise it was made with Echo RAM Fix off. * bit 7: reserved, set to 0. */ /* 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(r.ReadStringFixedAscii(12)); m.Header[HeaderKeys.GAMENAME] = gameName; // 030 1-byte unsigned char: minor version/revision number of current VBM version, the latest is "1" byte minorVersion = r.ReadByte(); m.Comments.Add(MOVIEORIGIN + " .VBM version " + majorVersion + "." + minorVersion); m.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 checksum_crc16 = 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 gameCode_unitCode = r.ReadUInt32(); if (platform == "GBA") { m.Header[CRC16] = checksum_crc16.ToString(); m.Header[GAMECODE] = gameCode_unitCode.ToString(); } else { m.Header[INTERNALCHECKSUM] = checksum_crc16.ToString(); m.Header[UNITCODE] = gameCode_unitCode.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(r.ReadStringFixedAscii(64)); m.Header[HeaderKeys.AUTHOR] = author; // The following 128 bytes are for a description of the movie. Both parts must be null-terminated. string movieDescription = NullTerminated(r.ReadStringFixedAscii(128)); m.Comments.Add(COMMENT + " " + movieDescription); r.BaseStream.Position = firstFrameOffset; SimpleController controllers = new SimpleController { Type = new ControllerDefinition() }; if (platform != "GBA") { controllers.Type.Name = "Gameboy Controller"; } else { controllers.Type.Name = "GBA Controller"; } /* * 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. if (warningMsg == "") { for (int button = 0; button < other.Length; button++) { if (((controllerState >> (button + 10)) & 0x1) != 0) { warningMsg = "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); } } m.AppendFrame(controllers); } r.Close(); fs.Close(); return m; }
private static BkmMovie ImportSMV(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 000 4-byte signature: 53 4D 56 1A "SMV\x1A" string signature = r.ReadStringFixedAscii(4); if (signature != "SMV\x1A") { errorMsg = "This is not a valid .SMV file."; r.Close(); fs.Close(); return null; } m.Header[HeaderKeys.PLATFORM] = "SNES"; // 004 4-byte little-endian unsigned int: version number uint versionNumber = r.ReadUInt32(); string version; switch (versionNumber) { case 1: version = "1.43"; break; case 4: version = "1.51"; break; case 5: version = "1.52"; break; default: errorMsg = "SMV version not recognized. 1.43, 1.51, and 1.52 are currently supported."; r.Close(); fs.Close(); return null; } m.Comments.Add(EMULATIONORIGIN + " Snes9x version " + version); m.Comments.Add(MOVIEORIGIN + " .SMV"); /* 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: rerecord count m.Rerecords = r.ReadUInt32(); // 010 4-byte little-endian unsigned int: number of frames uint frameCount = r.ReadUInt32(); // 014 1-byte flags "controller mask" byte controllerFlags = r.ReadByte(); /* * bit 0: controller 1 in use * bit 1: controller 2 in use * bit 2: controller 3 in use * bit 3: controller 4 in use * bit 4: controller 5 in use * other: reserved, set to 0 */ SimpleController controllers = new SimpleController { Type = new ControllerDefinition { Name = "SNES Controller" } }; bool[] controllersUsed = new bool[5]; for (int controller = 1; controller <= controllersUsed.Length; controller++) { controllersUsed[controller - 1] = (((controllerFlags >> (controller - 1)) & 0x1) != 0); } // 015 1-byte flags "movie options" byte movieFlags = r.ReadByte(); /* bit 0: if "0", movie begins from an embedded "quicksave" snapshot if "1", a SRAM is included instead of a quicksave; movie begins from reset */ if ((movieFlags & 0x1) == 0) { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } // bit 1: if "0", movie is NTSC (60 fps); if "1", movie is PAL (50 fps) bool pal = (((movieFlags >> 1) & 0x1) != 0); m.Header[HeaderKeys.PAL] = pal.ToString(); // other: reserved, set to 0 /* 016 1-byte flags "sync options": bit 0: MOVIE_SYNC2_INIT_FASTROM other: reserved, set to 0 */ r.ReadByte(); /* 017 1-byte flags "sync options": bit 0: MOVIE_SYNC_DATA_EXISTS if "1", all sync options flags are defined. if "0", all sync options flags have no meaning. bit 1: MOVIE_SYNC_WIP1TIMING bit 2: MOVIE_SYNC_LEFTRIGHT bit 3: MOVIE_SYNC_VOLUMEENVX bit 4: MOVIE_SYNC_FAKEMUTE bit 5: MOVIE_SYNC_SYNCSOUND bit 6: MOVIE_SYNC_HASROMINFO if "1", there is extra ROM info located right in between of the metadata and the savestate. bit 7: set to 0. */ byte syncFlags = r.ReadByte(); /* Extra ROM info is always positioned right before the savestate. Its size is 30 bytes if MOVIE_SYNC_HASROMINFO is used (and MOVIE_SYNC_DATA_EXISTS is set), 0 bytes otherwise. */ int extraRomInfo = (((syncFlags >> 6) & 0x1) != 0 && (syncFlags & 0x1) != 0) ? 30 : 0; // 018 4-byte little-endian unsigned int: offset to the savestate inside file uint savestateOffset = r.ReadUInt32(); // 01C 4-byte little-endian unsigned int: offset to the controller data inside file uint firstFrameOffset = r.ReadUInt32(); int[] controllerTypes = new int[2]; // The (.SMV 1.51 and up) header has an additional 32 bytes at the end if (version != "1.43") { // 020 4-byte little-endian unsigned int: number of input samples, primarily for peripheral-using games r.ReadBytes(4); /* 024 2 1-byte unsigned ints: what type of controller is plugged into ports 1 and 2 respectively: 0=NONE, 1=JOYPAD, 2=MOUSE, 3=SUPERSCOPE, 4=JUSTIFIER, 5=MULTITAP */ controllerTypes[0] = r.ReadByte(); controllerTypes[1] = r.ReadByte(); // 026 4 1-byte signed ints: controller IDs of port 1, or -1 for unplugged r.ReadBytes(4); // 02A 4 1-byte signed ints: controller IDs of port 2, or -1 for unplugged r.ReadBytes(4); // 02E 18 bytes: reserved for future use r.ReadBytes(18); } /* After the header comes "metadata", which is UTF16-coded movie title string (author info). The metadata begins from position 32 (0x20 (0x40 for 1.51 and up)) and ends at <savestate_offset - length_of_extra_rom_info_in_bytes>. */ byte[] metadata = r.ReadBytes((int)(savestateOffset - extraRomInfo - ((version != "1.43") ? 0x40 : 0x20))); string author = NullTerminated(Encoding.Unicode.GetString(metadata).Trim()); if (author != "") { m.Header[HeaderKeys.AUTHOR] = author; } if (extraRomInfo == 30) { // 000 3 bytes of zero padding: 00 00 00 003 4-byte integer: CRC32 of the ROM 007 23-byte ascii string r.ReadBytes(3); int crc32 = r.ReadInt32(); m.Header[CRC32] = crc32.ToString(); // the game name copied from the ROM, truncated to 23 bytes (the game name in the ROM is 21 bytes) string gameName = NullTerminated(Encoding.UTF8.GetString(r.ReadBytes(23))); m.Header[HeaderKeys.GAMENAME] = gameName; } r.BaseStream.Position = firstFrameOffset; /* 01 00 (reserved) 02 00 (reserved) 04 00 (reserved) 08 00 (reserved) 10 00 R 20 00 L 40 00 X 80 00 A 00 01 Right 00 02 Left 00 04 Down 00 08 Up 00 10 Start 00 20 Select 00 40 Y 00 80 B */ string[] buttons = { "Right", "Left", "Down", "Up", "Start", "Select", "Y", "B", "R", "L", "X", "A" }; for (int frame = 0; frame <= frameCount; frame++) { controllers["Reset"] = true; for (int player = 1; player <= controllersUsed.Length; player++) { if (!controllersUsed[player - 1]) { continue; } /* Each frame consists of 2 bytes per controller. So if there are 3 controllers, a frame is 6 bytes and if there is only 1 controller, a frame is 2 bytes. */ byte controllerState1 = r.ReadByte(); byte controllerState2 = r.ReadByte(); /* In the reset-recording patch, a frame that contains the value FF FF for every controller denotes a reset. The reset is done through the S9xSoftReset routine. */ if (controllerState1 != 0xFF || controllerState2 != 0xFF) { controllers["Reset"] = false; } /* While the meaning of controller data (for 1.51 and up) for a single standard SNES controller pad remains the same, each frame of controller data can contain additional bytes if input for peripherals is being recorded. */ if (version != "1.43" && player <= controllerTypes.Length) { var peripheral = string.Empty; switch (controllerTypes[player - 1]) { // NONE case 0: continue; // JOYPAD case 1: break; // MOUSE case 2: peripheral = "Mouse"; // 5*num_mouse_ports r.ReadBytes(5); break; // SUPERSCOPE case 3: peripheral = "Super Scope"; // 6*num_superscope_ports r.ReadBytes(6); break; // JUSTIFIER case 4: peripheral = "Justifier"; // 11*num_justifier_ports r.ReadBytes(11); break; // MULTITAP case 5: peripheral = "Multitap"; break; } if (peripheral != "" && warningMsg == "") { warningMsg = "Unable to import " + peripheral + "."; } } ushort controllerState = (ushort)(((controllerState1 << 4) & 0x0F00) | controllerState2); if (player <= BkmMnemonicConstants.PLAYERS[controllers.Type.Name]) { for (int button = 0; button < buttons.Length; button++) { controllers["P" + player + " " + buttons[button]] = ( ((controllerState >> button) & 0x1) != 0 ); } } else if (warningMsg == "") { warningMsg = "Controller " + player + " not supported."; } } // The controller data contains <number_of_frames + 1> frames. if (frame == 0) { continue; } m.AppendFrame(controllers); } r.Close(); fs.Close(); LibsnesCore.SnesSyncSettings ss = new LibsnesCore.SnesSyncSettings(); ss.Profile = "Compatibility"; m.SyncSettingsJson = ConfigService.SaveWithType(ss); return m; }
// NMV file format: http://tasvideos.org/NMV.html private static BkmMovie ImportNMV(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 000 4-byte signature: 4E 53 53 1A "NSS\x1A" string signature = r.ReadStringFixedAscii(4); if (signature != "NSS\x1A") { errorMsg = "This is not a valid .NMV file."; r.Close(); fs.Close(); return null; } // 004 4-byte version string (example "0960") string emuVersion = r.ReadStringFixedAscii(4); m.Comments.Add(EMULATIONORIGIN + " Nintendulator version " + emuVersion); m.Comments.Add(MOVIEORIGIN + " .NMV"); // 008 4-byte file size, not including the 16-byte header r.ReadUInt32(); /* 00C 4-byte file type string * "NSAV" - standard savestate * "NREC" - savestate saved during movie recording * "NMOV" - standalone movie file */ string type = r.ReadStringFixedAscii(4); if (type != "NMOV") { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } /* Individual blocks begin with an 8-byte header, consisting of a 4-byte signature and a 4-byte length (which does not include the length of the block header). The final block in the file is of type "NMOV" */ string header = r.ReadStringFixedAscii(4); if (header != "NMOV") { errorMsg = "This is not a valid .NMV file."; r.Close(); fs.Close(); return null; } r.ReadUInt32(); // 000 1-byte controller #1 type (see below) byte controller1 = r.ReadByte(); // 001 1-byte controller #2 type (or four-score mask, see below) byte controller2 = r.ReadByte(); /* Controller data is variant, depending on which controllers are attached at the time of recording. The following controllers are implemented: * 0 - Unconnected * 1 - Standard Controller (1 byte) * 2 - Zapper (3 bytes) * 3 - Arkanoid Paddle (2 bytes) * 4 - Power Pad (2 bytes) * 5 - Four-Score (special) * 6 - SNES controller (2 bytes) - A/B become B/Y, adds A/X and L/R shoulder buttons * 7 - Vs Unisystem Zapper (3 bytes) */ bool fourscore = (controller1 == 5); m.Header[HeaderKeys.FOURSCORE] = fourscore.ToString(); bool[] masks = new[] { false, false, false, false, false }; if (fourscore) { /* When a Four-Score is indicated for Controller #1, the Controller #2 byte becomes a bit mask to indicate which ports on the Four-Score have controllers connected to them. Each connected controller stores 1 byte per frame. Nintendulator's Four-Score recording is seemingly broken. */ for (int controller = 1; controller < masks.Length; controller++) { masks[controller - 1] = (((controller2 >> (controller - 1)) & 0x1) != 0); } warningMsg = "Nintendulator's Four Score recording is seemingly broken."; } else { byte[] types = new[] { controller1, controller2 }; for (int controller = 1; controller <= types.Length; controller++) { masks[controller - 1] = (types[controller - 1] == 1); // Get the first unsupported controller warning message that arises. if (warningMsg == "") { switch (types[controller - 1]) { case 0: break; case 2: warningMsg = "Zapper"; break; case 3: warningMsg = "Arkanoid Paddle"; break; case 4: warningMsg = "Power Pad"; break; case 5: warningMsg = "A Four Score in the second controller port is invalid."; continue; case 6: warningMsg = "SNES controller"; break; case 7: warningMsg = "Vs Unisystem Zapper"; break; } if (warningMsg != "") { warningMsg = warningMsg + " is not properly supported."; } } } } // 002 1-byte expansion port controller type byte expansion = r.ReadByte(); /* The expansion port can potentially have an additional controller connected. The following expansion controllers are implemented: * 0 - Unconnected * 1 - Famicom 4-player adapter (2 bytes) * 2 - Famicom Arkanoid paddle (2 bytes) * 3 - Family Basic Keyboard (currently does not support demo recording) * 4 - Alternate keyboard layout (currently does not support demo recording) * 5 - Family Trainer (2 bytes) * 6 - Oeka Kids writing tablet (3 bytes) */ string[] expansions = new[] { "Unconnected", "Famicom 4-player adapter", "Famicom Arkanoid paddle", "Family Basic Keyboard", "Alternate keyboard layout", "Family Trainer", "Oeka Kids writing tablet" }; if (expansion != 0 && warningMsg == "") { warningMsg = "Expansion port is not properly supported. This movie uses " + expansions[expansion] + "."; } // 003 1-byte number of bytes per frame, plus flags byte data = r.ReadByte(); int bytesPerFrame = data & 0xF; int bytes = 0; for (int controller = 1; controller < masks.Length; controller++) { if (masks[controller - 1]) { bytes++; } } /* Depending on the mapper used by the game in question, an additional byte of data may be stored during each frame. This is most frequently used for FDS games (storing either the disk number or 0xFF to eject) or VS Unisystem coin/DIP switch toggles (limited to 1 action per frame). This byte exists if the bytes per frame do not match up with the amount of bytes the controllers take up. */ if (bytes != bytesPerFrame) { masks[4] = true; } // bit 6: Game Genie active /* bit 7: Framerate * if "0", NTSC timing * if "1", "PAL" timing */ bool pal = (((data >> 7) & 0x1) != 0); m.Header[HeaderKeys.PAL] = pal.ToString(); // 004 4-byte little-endian unsigned int: rerecord count uint rerecordCount = r.ReadUInt32(); m.Rerecords = rerecordCount; /* 008 4-byte little-endian unsigned int: length of movie description 00C (variable) null-terminated UTF-8 text, movie description (currently not implemented) */ string movieDescription = NullTerminated(r.ReadStringFixedAscii((int)r.ReadUInt32())); m.Comments.Add(COMMENT + " " + movieDescription); // ... 4-byte little-endian unsigned int: length of controller data in bytes uint length = r.ReadUInt32(); // ... (variable) controller data SimpleController controllers = new SimpleController { Type = new ControllerDefinition { Name = "NES Controller" } }; /* Standard controllers store data in the following format: * 01: A * 02: B * 04: Select * 08: Start * 10: Up * 20: Down * 40: Left * 80: Right Other controllers store data in their own formats, and are beyond the scope of this document. */ string[] buttons = new[] { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" }; // The controller data contains <number_of_bytes> / <bytes_per_frame> frames. long frameCount = length / bytesPerFrame; for (int frame = 1; frame <= frameCount; frame++) { // Controller update data is emitted to the movie file during every frame. for (int player = 1; player <= masks.Length; player++) { if (!masks[player - 1]) { continue; } byte controllerState = r.ReadByte(); if (player != 5) { for (int button = 0; button < buttons.Length; button++) { controllers["P" + player + " " + buttons[button]] = (((controllerState >> button) & 0x1) != 0); } } else if (warningMsg == "") { warningMsg = "Extra input is not properly supported."; } } m.AppendFrame(controllers); } r.Close(); fs.Close(); return m; }
// MMV file format: http://tasvideos.org/MMV.html private static BkmMovie ImportMMV(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 0000: 4-byte signature: "MMV\0" string signature = r.ReadStringFixedAscii(4); if (signature != "MMV\0") { errorMsg = "This is not a valid .MMV file."; r.Close(); fs.Close(); return null; } // 0004: 4-byte little endian unsigned int: dega version uint emuVersion = r.ReadUInt32(); m.Comments.Add(EMULATIONORIGIN + " Dega version " + emuVersion); m.Comments.Add(MOVIEORIGIN + " .MMV"); // 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(); m.Rerecords = rerecordCount; // 0010: 4-byte little endian flag: begin from reset? uint reset = r.ReadUInt32(); if (reset == 0) { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } // 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(r.ReadStringFixedAscii(64)); m.Header[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); m.Header[HeaderKeys.PAL] = pal.ToString(); // bit 2: Japan bool japan = (((flags >> 2) & 0x1) != 0); m.Header[JAPAN] = japan.ToString(); // bit 3: Game Gear (version 1.16+) bool gamegear; if (((flags >> 3) & 0x1) != 0) { gamegear = true; m.Header[HeaderKeys.PLATFORM] = "GG"; } else { gamegear = false; m.Header[HeaderKeys.PLATFORM] = "SMS"; } // bits 4-31: unused r.ReadBytes(3); // 0064-00e3: string: rom name (ASCII) string gameName = NullTerminated(r.ReadStringFixedAscii(128)); m.Header[HeaderKeys.GAMENAME] = gameName; // 00e4-00f3: binary: rom MD5 digest byte[] md5 = r.ReadBytes(16); m.Header[MD5] = string.Format("{0:x8}", md5.BytesToHexString().ToLower()); var controllers = new SimpleController { Type = new ControllerDefinition { Name = "SMS Controller" } }; /* 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 && (!gamegear)) || (((controllerState >> 7) & 0x1) != 0 && gamegear) ); } } m.AppendFrame(controllers); } r.Close(); fs.Close(); return m; }
protected void parseTextInputLog(BinaryReader br, Bk2Movie movie, MiscHeaderInfo info) { Octoshock.SyncSettings settings = new Octoshock.SyncSettings(); SimpleController controllers = new SimpleController(); settings.FIOConfig.Devices8 = new[] { info.player1Type, OctoshockDll.ePeripheralType.None,OctoshockDll.ePeripheralType.None,OctoshockDll.ePeripheralType.None, info.player2Type, OctoshockDll.ePeripheralType.None,OctoshockDll.ePeripheralType.None,OctoshockDll.ePeripheralType.None }; controllers.Type = Octoshock.CreateControllerDefinition(settings); string[] buttons = { "Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left", "L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square"}; bool isCdTrayOpen = false; int cdNumber = 1; for (int frame = 0; frame < info.frameCount; ++frame) { if (info.player1Type != OctoshockDll.ePeripheralType.None) { // As L3 and R3 don't exist on a standard gamepad, handle them separately later. Unfortunately // due to the layout, we handle select separately too first. controllers["P1 Select"] = br.ReadChar() != '.'; if (info.player1Type == OctoshockDll.ePeripheralType.DualShock) { controllers["P1 L3"] = br.ReadChar() != '.'; controllers["P1 R3"] = br.ReadChar() != '.'; } for (int button = 3; button < buttons.Length; button++) { controllers["P1 " + buttons[button]] = br.ReadChar() != '.'; } if (info.player1Type == OctoshockDll.ePeripheralType.DualShock) { // The analog controls are encoded as four space-separated numbers with a leading space string leftXRaw = new string(br.ReadChars(4)).Trim(); string leftYRaw = new string(br.ReadChars(4)).Trim(); string rightXRaw = new string(br.ReadChars(4)).Trim(); string rightYRaw = new string(br.ReadChars(4)).Trim(); Tuple<string, float> leftX = new Tuple<string, float>("P1 LStick X", float.Parse(leftXRaw)); Tuple<string, float> leftY = new Tuple<string, float>("P1 LStick Y", float.Parse(leftYRaw)); Tuple<string, float> rightX = new Tuple<string, float>("P1 RStick X", float.Parse(rightXRaw)); Tuple<string, float> rightY = new Tuple<string, float>("P1 RStick Y", float.Parse(rightYRaw)); controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY }); } } // Each controller is terminated with a pipeline. br.ReadChar(); if (info.player2Type != OctoshockDll.ePeripheralType.None) { // As L3 and R3 don't exist on a standard gamepad, handle them separately later. Unfortunately // due to the layout, we handle select separately too first. controllers["P2 Select"] = br.ReadChar() != '.'; if (info.player2Type == OctoshockDll.ePeripheralType.DualShock) { controllers["P2 L3"] = br.ReadChar() != '.'; controllers["P2 R3"] = br.ReadChar() != '.'; } for (int button = 3; button < buttons.Length; button++) { controllers["P2 " + buttons[button]] = br.ReadChar() != '.'; } if (info.player2Type == OctoshockDll.ePeripheralType.DualShock) { // The analog controls are encoded as four space-separated numbers with a leading space string leftXRaw = new string(br.ReadChars(4)).Trim(); string leftYRaw = new string(br.ReadChars(4)).Trim(); string rightXRaw = new string(br.ReadChars(4)).Trim(); string rightYRaw = new string(br.ReadChars(4)).Trim(); Tuple<string, float> leftX = new Tuple<string, float>("P2 LStick X", float.Parse(leftXRaw)); Tuple<string, float> leftY = new Tuple<string, float>("P2 LStick Y", float.Parse(leftYRaw)); Tuple<string, float> rightX = new Tuple<string, float>("P2 RStick X", float.Parse(rightXRaw)); Tuple<string, float> rightY = new Tuple<string, float>("P2 RStick Y", float.Parse(rightYRaw)); controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY }); } } // Each controller is terminated with a pipeline. br.ReadChar(); byte controlState = br.ReadByte(); controllers["Reset"] = (controlState & 0x02) != 0; if ((controlState & 0x04) != 0) { if (isCdTrayOpen) { controllers["Close"] = true; cdNumber++; } else { controllers["Open"] = true; } isCdTrayOpen = !isCdTrayOpen; } else { controllers["Close"] = false; controllers["Open"] = false; } Tuple<string, float> discSelect = new Tuple<string, float>("Disc Select", cdNumber); controllers.AcceptNewFloats(new[] { discSelect }); if ((controlState & 0xFC) != 0) { Result.Warnings.Add("Ignored toggle hack flag on frame " + frame.ToString()); } // Each controller is terminated with a pipeline. br.ReadChar(); movie.AppendFrame(controllers); } }
// FMV file format: http://tasvideos.org/FMV.html private static BkmMovie ImportFMV(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 000 4-byte signature: 46 4D 56 1A "FMV\x1A" string signature = r.ReadStringFixedAscii(4); if (signature != "FMV\x1A") { errorMsg = "This is not a valid .FMV file."; r.Close(); fs.Close(); return null; } // 004 1-byte flags: byte flags = r.ReadByte(); // bit 7: 0=reset-based, 1=savestate-based if (((flags >> 2) & 0x1) != 0) { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } // 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; m.Header[HeaderKeys.BOARDNAME] = "FDS"; } else { FDS = false; } m.Header[HeaderKeys.PLATFORM] = "NES"; // 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. */ m.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(r.ReadStringFixedAscii(64)); m.Comments.Add(EMULATIONORIGIN + " Famtasia version " + emuVersion); m.Comments.Add(MOVIEORIGIN + " .FMV"); // 050 64-byte zero-terminated movie title string string description = NullTerminated(r.ReadStringFixedAscii(64)); m.Comments.Add(COMMENT + " " + description); if (!controller1 && !controller2 && !FDS) { warningMsg = "No input recorded."; r.Close(); fs.Close(); return m; } /* The file format has no means of identifying NTSC/"PAL". It is always assumed that the game is NTSC - that is, 60 fps. */ m.Header[HeaderKeys.PAL] = "False"; // 090 frame data begins here SimpleController controllers = new SimpleController { Type = new ControllerDefinition { Name = "NES Controller" } }; /* * 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 { warningMsg = "FDS commands are not properly supported."; } } m.AppendFrame(controllers); } r.Close(); fs.Close(); return m; }
protected void parseTextInputLog(BinaryReader br, Bk2Movie movie, MiscHeaderInfo info) { Octoshock.SyncSettings settings = new Octoshock.SyncSettings(); SimpleController controllers = new SimpleController(); settings.FIOConfig.Devices8 = new[] { info.player1Type, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, info.player2Type, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None }; controllers.Type = Octoshock.CreateControllerDefinition(settings); string[] buttons = { "Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left", "L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square" }; bool isCdTrayOpen = false; int cdNumber = 1; for (int frame = 0; frame < info.frameCount; ++frame) { if (info.player1Type != OctoshockDll.ePeripheralType.None) { // As L3 and R3 don't exist on a standard gamepad, handle them separately later. Unfortunately // due to the layout, we handle select separately too first. controllers["P1 Select"] = br.ReadChar() != '.'; if (info.player1Type == OctoshockDll.ePeripheralType.DualShock) { controllers["P1 L3"] = br.ReadChar() != '.'; controllers["P1 R3"] = br.ReadChar() != '.'; } for (int button = 3; button < buttons.Length; button++) { controllers["P1 " + buttons[button]] = br.ReadChar() != '.'; } if (info.player1Type == OctoshockDll.ePeripheralType.DualShock) { // The analog controls are encoded as four space-separated numbers with a leading space string leftXRaw = new string(br.ReadChars(4)).Trim(); string leftYRaw = new string(br.ReadChars(4)).Trim(); string rightXRaw = new string(br.ReadChars(4)).Trim(); string rightYRaw = new string(br.ReadChars(4)).Trim(); Tuple <string, float> leftX = new Tuple <string, float>("P1 LStick X", float.Parse(leftXRaw)); Tuple <string, float> leftY = new Tuple <string, float>("P1 LStick Y", float.Parse(leftYRaw)); Tuple <string, float> rightX = new Tuple <string, float>("P1 RStick X", float.Parse(rightXRaw)); Tuple <string, float> rightY = new Tuple <string, float>("P1 RStick Y", float.Parse(rightYRaw)); controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY }); } } // Each controller is terminated with a pipeline. br.ReadChar(); if (info.player2Type != OctoshockDll.ePeripheralType.None) { // As L3 and R3 don't exist on a standard gamepad, handle them separately later. Unfortunately // due to the layout, we handle select separately too first. controllers["P2 Select"] = br.ReadChar() != '.'; if (info.player2Type == OctoshockDll.ePeripheralType.DualShock) { controllers["P2 L3"] = br.ReadChar() != '.'; controllers["P2 R3"] = br.ReadChar() != '.'; } for (int button = 3; button < buttons.Length; button++) { controllers["P2 " + buttons[button]] = br.ReadChar() != '.'; } if (info.player2Type == OctoshockDll.ePeripheralType.DualShock) { // The analog controls are encoded as four space-separated numbers with a leading space string leftXRaw = new string(br.ReadChars(4)).Trim(); string leftYRaw = new string(br.ReadChars(4)).Trim(); string rightXRaw = new string(br.ReadChars(4)).Trim(); string rightYRaw = new string(br.ReadChars(4)).Trim(); Tuple <string, float> leftX = new Tuple <string, float>("P2 LStick X", float.Parse(leftXRaw)); Tuple <string, float> leftY = new Tuple <string, float>("P2 LStick Y", float.Parse(leftYRaw)); Tuple <string, float> rightX = new Tuple <string, float>("P2 RStick X", float.Parse(rightXRaw)); Tuple <string, float> rightY = new Tuple <string, float>("P2 RStick Y", float.Parse(rightYRaw)); controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY }); } } // Each controller is terminated with a pipeline. br.ReadChar(); byte controlState = br.ReadByte(); controllers["Reset"] = (controlState & 0x02) != 0; if ((controlState & 0x04) != 0) { if (isCdTrayOpen) { controllers["Close"] = true; cdNumber++; } else { controllers["Open"] = true; } isCdTrayOpen = !isCdTrayOpen; } else { controllers["Close"] = false; controllers["Open"] = false; } Tuple <string, float> discSelect = new Tuple <string, float>("Disc Select", cdNumber); controllers.AcceptNewFloats(new[] { discSelect }); if ((controlState & 0xFC) != 0) { Result.Warnings.Add("Ignored toggle hack flag on frame " + frame.ToString()); } // Each controller is terminated with a pipeline. br.ReadChar(); movie.AppendFrame(controllers); } }
protected void parseBinaryInputLog(BinaryReader br, Bk2Movie movie, MiscHeaderInfo info) { Octoshock.SyncSettings settings = new Octoshock.SyncSettings(); SimpleController controllers = new SimpleController(); settings.FIOConfig.Devices8 = new[] { info.player1Type, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, info.player2Type, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None }; controllers.Type = Octoshock.CreateControllerDefinition(settings); string[] buttons = { "Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left", "L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square" }; bool isCdTrayOpen = false; int cdNumber = 1; for (int frame = 0; frame < info.frameCount; ++frame) { if (info.player1Type != OctoshockDll.ePeripheralType.None) { UInt16 controllerState = br.ReadUInt16(); // As L3 and R3 don't exist on a standard gamepad, handle them separately later. Unfortunately // due to the layout, we handle select separately too first. controllers["P1 Select"] = (controllerState & 0x1) != 0; for (int button = 3; button < buttons.Length; button++) { controllers["P1 " + buttons[button]] = (((controllerState >> button) & 0x1) != 0); if (((controllerState >> button) & 0x1) != 0 && button > 15) { continue; } } if (info.player1Type == OctoshockDll.ePeripheralType.DualShock) { controllers["P1 L3"] = (controllerState & 0x2) != 0; controllers["P1 R3"] = (controllerState & 0x4) != 0; Tuple <string, float> leftX = new Tuple <string, float>("P1 LStick X", (float)br.ReadByte()); Tuple <string, float> leftY = new Tuple <string, float>("P1 LStick Y", (float)br.ReadByte()); Tuple <string, float> rightX = new Tuple <string, float>("P1 RStick X", (float)br.ReadByte()); Tuple <string, float> rightY = new Tuple <string, float>("P1 RStick Y", (float)br.ReadByte()); controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY }); } } if (info.player2Type != OctoshockDll.ePeripheralType.None) { UInt16 controllerState = br.ReadUInt16(); for (int button = 0; button < buttons.Length; button++) { controllers["P2 " + buttons[button]] = (((controllerState >> button) & 0x1) != 0); if (((controllerState >> button) & 0x1) != 0 && button > 15) { continue; } } if (info.player2Type == OctoshockDll.ePeripheralType.DualShock) { Tuple <string, float> leftX = new Tuple <string, float>("P2 LStick X", (float)br.ReadByte()); Tuple <string, float> leftY = new Tuple <string, float>("P2 LStick Y", (float)br.ReadByte()); Tuple <string, float> rightX = new Tuple <string, float>("P2 RStick X", (float)br.ReadByte()); Tuple <string, float> rightY = new Tuple <string, float>("P2 RStick Y", (float)br.ReadByte()); controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY }); } } byte controlState = br.ReadByte(); controllers["Reset"] = (controlState & 0x02) != 0; if ((controlState & 0x04) != 0) { if (isCdTrayOpen) { controllers["Close"] = true; cdNumber++; } else { controllers["Open"] = true; } isCdTrayOpen = !isCdTrayOpen; } else { controllers["Close"] = false; controllers["Open"] = false; } Tuple <string, float> discSelect = new Tuple <string, float>("Disc Select", cdNumber); controllers.AcceptNewFloats(new[] { discSelect }); if ((controlState & 0xFC) != 0) { Result.Warnings.Add("Ignored toggle hack flag on frame " + frame.ToString()); } movie.AppendFrame(controllers); } }
// ZMV file format: http://tasvideos.org/ZMV.html private static BkmMovie ImportZMV(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 000 3-byte signature: 5A 4D 56 "ZMV" string signature = r.ReadStringFixedAscii(3); if (signature != "ZMV") { errorMsg = "This is not a valid .ZMV file."; r.Close(); fs.Close(); return null; } m.Header[HeaderKeys.PLATFORM] = "SNES"; // 003 2-byte little-endian unsigned int: zsnes version number short version = r.ReadInt16(); m.Comments.Add(EMULATIONORIGIN + " ZSNES version " + version); m.Comments.Add(MOVIEORIGIN + " .ZMV"); // 005 4-byte little-endian integer: CRC32 of the ROM int crc32 = r.ReadInt32(); m.Header[CRC32] = crc32.ToString(); // 009 4-byte little-endian unsigned int: number of frames uint frameCount = r.ReadUInt32(); // 00D 4-byte little-endian unsigned int: number of rerecords uint rerecordCount = r.ReadUInt32(); m.Rerecords = rerecordCount; // 011 4-byte little-endian unsigned int: number of frames removed by rerecord r.ReadBytes(4); // 015 4-byte little-endian unsigned int: number of frames advanced step by step r.ReadBytes(4); // 019 1-byte: average recording frames per second r.ReadByte(); // 01A 4-byte little-endian unsigned int: number of key combos r.ReadBytes(4); // 01E 2-byte little-endian unsigned int: number of internal chapters ushort internalChaptersCount = r.ReadUInt16(); // 020 2-byte little-endian unsigned int: length of the author name field in bytes ushort authorSize = r.ReadUInt16(); // 022 3-byte little-endian unsigned int: size of an uncompressed save state in bytes r.ReadBytes(3); // 025 1-byte: reserved r.ReadByte(); /* 026 1-byte flags: initial input configuration bit 7: first input enabled bit 6: second input enabled bit 5: third input enabled bit 4: fourth input enabled bit 3: fifth input enabled bit 2: mouse in first port bit 1: mouse in second port bit 0: super scope in second port */ byte controllerFlags = r.ReadByte(); string peripheral = ""; bool superScope = ((controllerFlags & 0x1) != 0); if (superScope) { peripheral = "Super Scope"; } controllerFlags >>= 1; bool secondMouse = ((controllerFlags & 0x1) != 0); if (secondMouse && peripheral == "") { peripheral = "Second Mouse"; } controllerFlags >>= 1; bool firstMouse = ((controllerFlags & 0x1) != 0); if (firstMouse && peripheral == "") { peripheral = "First Mouse"; } if (peripheral != "") { warningMsg = "Unable to import " + peripheral + "."; } // 027 1-byte flags: byte movieFlags = r.ReadByte(); byte begins = (byte)(movieFlags & 0xC0); /* bits 7,6: if "00", movie begins from savestate */ if (begins == 0x00) { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } // if "10", movie begins from reset // if "01", movie begins from power-on if (begins == 0x40) { errorMsg = "Movies that begin with SRAM are not supported."; r.Close(); fs.Close(); return null; } // if "11", movie begins from power-on with SRAM clear // bit 5: if "0", movie is NTSC (60 fps); if "1", movie is PAL (50 fps) bool pal = (((movieFlags >> 5) & 0x1) != 0); m.Header[HeaderKeys.PAL] = pal.ToString(); // other: reserved, set to 0 /* 028 3-byte little-endian unsigned int: initial save state size, highest bit specifies compression, next 23 specifies size */ uint savestateSize = (uint)((r.ReadByte() | (r.ReadByte() << 8) | (r.ReadByte() << 16)) & 0x7FFFFF); // Next follows a ZST format savestate. r.ReadBytes((int)savestateSize); SimpleController controllers = new SimpleController { Type = new ControllerDefinition { Name = "SNES Controller" } }; /* * bit 11: A * bit 10: X * bit 9: L * bit 8: R * bit 7: B * bit 6: Y * bit 5: Select * bit 4: Start * bit 3: Up * bit 2: Down * bit 1: Left * bit 0: Right */ string[] buttons = { "Right", "Left", "Down", "Up", "Start", "Select", "Y", "B", "R", "L", "X", "A" }; int frames = 1; int internalChapters = 1; while (frames <= frameCount || internalChapters <= internalChaptersCount) { /* 000 1-byte flags: bit 7: "1" if controller 1 changed, "0" otherwise bit 6: "1" if controller 2 changed, "0" otherwise bit 5: "1" if controller 3 changed, "0" otherwise bit 4: "1" if controller 4 changed, "0" otherwise bit 3: "1" if controller 5 changed, "0" otherwise bit 2: "1" if this is a "chapter" update, "0" if it's a "controller" update bit 1: "1" if this is RLE data instead of any of the above bit 0: "1" if this is command data instead any of the above */ byte flag = r.ReadByte(); if ((flag & 0x1) != 0) { /* If the event is a command, the other seven bits define the command. The command could be "reset now" or similar things. */ flag >>= 1; if (flag == 0x0) { controllers["Reset"] = true; m.AppendFrame(controllers); controllers["Reset"] = false; } // TODO: Other commands. } else if (((flag >> 1) & 0x1) != 0) { // If the event is RLE data, next follows 4 bytes which is the frame to repeat current input till. uint frame = r.ReadUInt32(); if (frame > frameCount) { throw new ArgumentException("RLE data repeats for frames beyond the total frame count."); } for (; frames <= frame; frames++) { m.AppendFrame(controllers); } } else if (((flag >> 2) & 0x1) != 0) { /* If the event is a "chapter" update, the packet follows with a ZST format savestate. Using a header of: 000 3-byte little endian unsigned int: save state size in format defined above */ savestateSize = (uint)((r.ReadByte() | (r.ReadByte() << 8) | (r.ReadByte() << 16)) & 0x7FFFFF); // 001 above size: save state r.ReadBytes((int)savestateSize); // above size+001 4-byte little endian unsigned int: frame number save state loads to r.ReadBytes(4); // above size+005 2-byte: controller status bit field, see below r.ReadBytes(2); // above size+007 9-byte: previous controller input bits r.ReadBytes(9); internalChapters++; } else { flag >>= 3; /* If the event is a "controller" update, next comes the controller data for each changed controller, 12 bits per controller, or 20 bits in the case of the super scope, zeropadding up to full bytes. The minimum length of the controller data is 2 bytes, and the maximum length is 9 bytes. */ bool leftOver = false; byte leftOverValue = 0x0; for (int player = 1; player <= 5; player++) { // If the controller has changed: if (((flag >> (5 - player)) & 0x1) != 0) { /* For example, if this frame contained input for controllers 1 and 2: byte 1: bit 7: P1 B bit 6: P1 Y bit 5: P1 Select bit 4: P1 Start bit 3: P1 Up bit 2: P1 Down bit 1: P1 Left bit 0: P1 Right byte 2: bit 7: P2 B bit 6: P2 Y bit 5: P2 Select bit 4: P2 Start bit 3: P1 A bit 2: P1 X bit 1: P1 L bit 0: P1 R byte 3: bit 7: P2 Up bit 6: P2 Down bit 5: P2 Left bit 4: P2 Right bit 3: P2 A bit 2: P2 X bit 1: P2 L bit 0: P2 R */ byte controllerState1 = r.ReadByte(); uint controllerState; if (!leftOver) { byte controllerState2 = r.ReadByte(); if (player == 2 && superScope) { byte controllerState3 = r.ReadByte(); controllerState = (uint)((controllerState1 | (controllerState2 << 8) | (controllerState3 << 12)) & 0x0FFFFF); leftOverValue = (byte)((controllerState3 >> 4) & 0x0F); } else { controllerState = (uint)((controllerState1 | (controllerState2 << 4)) & 0x0FFF); leftOverValue = (byte)((controllerState2 >> 4) & 0x0F); } } else { controllerState = (uint)((controllerState1 >> 4) | (leftOverValue << 4) | ((controllerState1 << 8) & 0x0F00)); if (player == 2 && superScope) { byte controllerState2 = r.ReadByte(); controllerState |= (uint)(controllerState2 << 12); } } leftOver = !leftOver; if (player <= BkmMnemonicConstants.PLAYERS[controllers.Type.Name]) { if (player != 2 || !superScope) { for (int button = 0; button < buttons.Length; button++) { controllers["P" + player + " " + buttons[button]] = ( ((controllerState >> button) & 0x1) != 0 ); } } } else if (warningMsg == "") { warningMsg = "Controller " + player + " not supported."; } } } m.AppendFrame(controllers); frames++; } } r.BaseStream.Position = r.BaseStream.Length - authorSize; // Last in the file comes the author name field, which is an UTF-8 encoded text string. var author = Encoding.UTF8.GetString(r.ReadBytes(authorSize)); m.Header[HeaderKeys.AUTHOR] = author; return m; }
/* MCM file format: http://code.google.com/p/mednafen-rr/wiki/MCM Mednafen-rr switched to MC2 from r261, so see r260 for details. */ private static BkmMovie ImportMCM(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 000 8-byte "MDFNMOVI" signature var signature = r.ReadStringFixedAscii(8); if (signature != "MDFNMOVI") { errorMsg = "This is not a valid .MCM file."; r.Close(); fs.Close(); return null; } // 008 uint32 Mednafen Version (Current is 0A 08) uint emuVersion = r.ReadUInt32(); m.Comments.Add(EMULATIONORIGIN + " Mednafen " + emuVersion); // 00C uint32 Movie Format Version (Current is 01) uint version = r.ReadUInt32(); m.Comments.Add(MOVIEORIGIN + " .MCM version " + version); // 010 32-byte MD5 of the ROM used byte[] md5 = r.ReadBytes(16); // Discard the second 16 bytes. r.ReadBytes(16); m.Header[MD5] = md5.BytesToHexString().ToLower(); // 030 64-byte Filename of the ROM used (with extension) string gameName = NullTerminated(r.ReadStringFixedAscii(64)); m.Header[HeaderKeys.GAMENAME] = gameName; // 070 uint32 Re-record Count uint rerecordCount = r.ReadUInt32(); m.Rerecords = (ulong)rerecordCount; // 074 5-byte Console indicator (pce, ngp, pcfx, wswan) string platform = NullTerminated(r.ReadStringFixedAscii(5)); Dictionary<string, Dictionary<string, object>> platforms = new Dictionary<string, Dictionary<string, object>> { { /* Normally, NES receives from 5 input ports, where the first 4 have a length of 1 byte, and the last has a length of 0. For the sake of simplicity, it is interpreted as 4 ports of 1 byte length for re-recording. */ "nes", new Dictionary<string, object> { {"name", "NES"}, {"ports", 4}, {"bytesPerPort", 1}, {"buttons", new[] { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" }} } }, { "pce", new Dictionary<string, object> { {"name", "PC Engine"}, {"ports", 5}, {"bytesPerPort", 2}, {"buttons", new[] { "B1", "B2", "Select", "Run", "Up", "Right", "Down", "Left" }} } }, { "lynx", new Dictionary<string, object> { { "name", "Lynx" }, { "ports", 2 }, { "bytesPerPort", 1 }, { "buttons", new[] { "A", "B", "Up", "Down", "Left", "Right" }} } } }; if (!platforms.ContainsKey(platform)) { errorMsg = "Platform " + platform + " not supported."; r.Close(); fs.Close(); return null; } string name = (string)platforms[platform]["name"]; string systemID = name.ToUpper(); // Hack m.Header[HeaderKeys.PLATFORM] = systemID; // 079 32-byte Author name string author = NullTerminated(r.ReadStringFixedAscii(32)); m.Header[HeaderKeys.AUTHOR] = author; // 099 103-byte Padding 0s r.ReadBytes(103); // TODO: Verify if NTSC/"PAL" mode used for the movie can be detected or not. // 100 variable Input data SimpleController controllers = new SimpleController { Type = new ControllerDefinition { Name = name + " Controller" } }; int bytes = 256; // The input stream consists of 1 byte for power-on and reset, and then X bytes per each input port per frame. if (platform == "nes") { // Power-on. r.ReadByte(); bytes++; } string[] buttons = (string[])platforms[platform]["buttons"]; int ports = (int)platforms[platform]["ports"]; int bytesPerPort = (int)platforms[platform]["bytesPerPort"]; // Frame Size (with Control Byte) int size = (ports * bytesPerPort) + 1; long frameCount = (fs.Length - bytes) / size; for (int frame = 1; frame <= frameCount; frame++) { for (int player = 1; player <= ports; player++) { if (bytesPerPort == 2) { // Discard the first byte. r.ReadByte(); } ushort controllerState = r.ReadByte(); for (int button = 0; button < buttons.Length; button++) { string prefix = platform == "lynx" ? "" : ("P" + player + " "); // hack controllers[prefix + buttons[button]] = (((controllerState >> button) & 0x1) != 0); } } r.ReadByte(); if (platform == "nes" && warningMsg == "") { warningMsg = "Control commands are not properly supported."; } m.AppendFrame(controllers); } r.Close(); fs.Close(); return m; }
// FCM file format: http://code.google.com/p/fceu/wiki/FCM private static BkmMovie ImportFCM(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 000 4-byte signature: 46 43 4D 1A "FCM\x1A" string signature = r.ReadStringFixedAscii(4); if (signature != "FCM\x1A") { errorMsg = "This is not a valid .FCM file."; r.Close(); fs.Close(); return null; } // 004 4-byte little-endian unsigned int: version number, must be 2 uint version = r.ReadUInt32(); if (version != 2) { errorMsg = ".FCM movie version must always be 2."; r.Close(); fs.Close(); return null; } m.Comments.Add(MOVIEORIGIN + " .FCM version " + version); // 008 1-byte flags byte flags = r.ReadByte(); // bit 0: reserved, set to 0 /* bit 1: * if "0", movie begins from an embedded "quicksave" snapshot * if "1", movie begins from reset or power-on[1] */ if (((flags >> 1) & 0x1) == 0) { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } /* bit 2: * if "0", NTSC timing * if "1", "PAL" timing Starting with version 0.98.12 released on September 19, 2004, a "PAL" flag was added to the header but unfortunately it is not reliable - the emulator does not take the "PAL" setting from the ROM, but from a user preference. This means that this site cannot calculate movie lengths reliably. */ bool pal = (((flags >> 2) & 0x1) != 0); m.Header[HeaderKeys.PAL] = pal.ToString(); // other: reserved, set to 0 bool syncHack = (((flags >> 4) & 0x1) != 0); m.Comments.Add(SYNCHACK + " " + syncHack); // 009 1-byte flags: reserved, set to 0 r.ReadByte(); // 00A 1-byte flags: reserved, set to 0 r.ReadByte(); // 00B 1-byte flags: reserved, set to 0 r.ReadByte(); // 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(); m.Rerecords = rerecordCount; /* 018 4-byte little-endian unsigned int: offset to the savestate inside file The savestate offset is <header_size + length_of_metadata_in_bytes + padding>. The savestate offset should be 4-byte aligned. At the savestate offset there is a savestate file. The savestate exists even if the movie is reset-based. */ r.ReadUInt32(); // 01C 4-byte little-endian unsigned int: offset to the controller data inside file uint firstFrameOffset = r.ReadUInt32(); // 020 16-byte md5sum of the ROM used byte[] md5 = r.ReadBytes(16); m.Header[MD5] = md5.BytesToHexString().ToLower(); // 030 4-byte little-endian unsigned int: version of the emulator used uint emuVersion = r.ReadUInt32(); m.Comments.Add(EMULATIONORIGIN + " FCEU " + emuVersion); // 034 name of the ROM used - UTF8 encoded nul-terminated string. List<byte> gameBytes = new List<byte>(); while (r.PeekChar() != 0) gameBytes.Add(r.ReadByte()); // Advance past null byte. r.ReadByte(); string gameName = Encoding.UTF8.GetString(gameBytes.ToArray()); m.Header[HeaderKeys.GAMENAME] = gameName; /* After the header comes "metadata", which is UTF8-coded movie title string. The metadata begins after the ROM name and ends at the savestate offset. This string is displayed as "Author Info" in the Windows version of the emulator. */ List<byte> authorBytes = new List<byte>(); while (r.PeekChar() != 0) authorBytes.Add(r.ReadByte()); // Advance past null byte. r.ReadByte(); string author = Encoding.UTF8.GetString(authorBytes.ToArray()); m.Header[HeaderKeys.AUTHOR] = author; // Advance to first byte of input data. r.BaseStream.Position = firstFrameOffset; SimpleController controllers = new SimpleController { Type = new ControllerDefinition { Name = "NES Controller" } }; string[] buttons = { "A", "B", "Select", "Start", "Up", "Down", "Left", "Right" }; bool fds = false; bool fourscore = false; int frame = 1; while (frame <= frameCount) { byte update = r.ReadByte(); // aa: Number of delta bytes to follow int delta = (update >> 5) & 0x3; int frames = 0; /* The delta byte(s) indicate the number of emulator frames between this update and the next update. It is encoded in little-endian format and its size depends on the magnitude of the delta: Delta of: Number of bytes: 0 0 1-255 1 256-65535 2 65536-(2^24-1) 3 */ for (int b = 0; b < delta; b++) { frames += r.ReadByte() * (int)Math.Pow(2, b * 8); } frame += frames; while (frames > 0) { m.AppendFrame(controllers); if (controllers["Reset"]) { controllers["Reset"] = false; } frames--; } if (((update >> 7) & 0x1) != 0) { // Control update: 1aabbbbb bool reset = false; int command = update & 0x1F; // bbbbb: controllers["Reset"] = (command == 1); if (warningMsg == "") { switch (command) { // Do nothing case 0: break; // Reset case 1: reset = true; break; // Power cycle case 2: reset = true; if (frame != 1) { warningMsg = "hard reset"; } break; // VS System Insert Coin case 7: warningMsg = "VS System Insert Coin"; break; // VS System Dipswitch 0 Toggle case 8: warningMsg = "VS System Dipswitch 0 Toggle"; break; // FDS Insert case 24: fds = true; warningMsg = "FDS Insert"; break; // FDS Eject case 25: fds = true; warningMsg = "FDS Eject"; break; // FDS Select Side case 26: fds = true; warningMsg = "FDS Select Side"; break; default: warningMsg = "unknown"; break; } if (warningMsg != "") { warningMsg = "Unable to import " + warningMsg + " command at frame " + frame + "."; } } /* 1 Even if the header says "movie begins from reset", the file still contains a quicksave, and the quicksave is actually loaded. This flag can't therefore be trusted. To check if the movie actually begins from reset, one must analyze the controller data and see if the first non-idle command in the file is a Reset or Power Cycle type control command. */ if (!reset && frame == 1) { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } } else { /* Controller update: 0aabbccc * bb: Gamepad number minus one (?) */ int player = ((update >> 3) & 0x3) + 1; if (player > 2) { fourscore = true; } /* ccc: * 0 A * 1 B * 2 Select * 3 Start * 4 Up * 5 Down * 6 Left * 7 Right */ int button = update & 0x7; /* The controller update toggles the affected input. Controller update data is emitted to the movie file only when the state of the controller changes. */ controllers["P" + player + " " + buttons[button]] = !controllers["P" + player + " " + buttons[button]]; } } m.Header[HeaderKeys.PLATFORM] = "NES"; if (fds) { m.Header[HeaderKeys.BOARDNAME] = "FDS"; } m.Header[HeaderKeys.FOURSCORE] = fourscore.ToString(); r.Close(); fs.Close(); return m; }
protected void ParseTextInputLog(BinaryReader br, IMovie movie, MiscHeaderInfo info) { Octoshock.SyncSettings settings = new Octoshock.SyncSettings(); SimpleController controllers = new SimpleController(); settings.FIOConfig.Devices8 = new[] { info.Player1Type, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, info.Player2Type, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None }; controllers.Definition = Octoshock.CreateControllerDefinition(settings); string[] buttons = { "Start", "Up", "Right", "Down", "Left", "L2", "R2", "L1", "R1", "Triangle","Circle","Cross", "Square" }; bool isCdTrayOpen = false; int cdNumber = 1; int player1Count = info.Player1Type == OctoshockDll.ePeripheralType.None ? 1 : info.Player1Type == OctoshockDll.ePeripheralType.Pad ? 15 : 33; int player2Count = info.Player2Type == OctoshockDll.ePeripheralType.None ? 1 : info.Player2Type == OctoshockDll.ePeripheralType.Pad ? 15 : 33; int strCount = player1Count + player2Count + 4; // 2 for control byte and pipe and line feed chars for (int frame = 0; frame < info.FrameCount; ++frame) { var mnemonicStr = new string(br.ReadChars(strCount)); // Junk whitespace at the end of a file if (string.IsNullOrWhiteSpace(mnemonicStr)) { continue; } // Gross, if not CR LF, this will fail, but will the PSXjin? if (!mnemonicStr.EndsWith("|\r\n")) { Result.Errors.Add("Unable to parse text input, unknown configuration"); } var split = mnemonicStr.Replace("\r\n", "").Split('|'); var player1Str = split[0]; var player2Str = split[1]; var controlStr = split[2]; if (info.Player1Type != OctoshockDll.ePeripheralType.None) { // As L3 and R3 don't exist on a standard gamepad, handle them separately later. Unfortunately // due to the layout, we handle select separately too first. controllers["P1 Select"] = player1Str[0] != '.'; if (info.Player1Type == OctoshockDll.ePeripheralType.DualShock) { controllers["P1 L3"] = player1Str[1] != '.'; controllers["P1 R3"] = player1Str[2] != '.'; } int offSet = info.Player1Type == OctoshockDll.ePeripheralType.Pad ? 0 : 2; for (int button = 1; button < buttons.Length; button++) { controllers[$"P1 {buttons[button]}"] = player1Str[button + offSet] != '.'; } if (info.Player1Type == OctoshockDll.ePeripheralType.DualShock) { // The analog controls are encoded as four space-separated numbers with a leading space string leftXRaw = player1Str.Substring(16, 4); string leftYRaw = player1Str.Substring(20, 4); string rightXRaw = player1Str.Substring(24, 4); string rightYRaw = player1Str.Substring(28, 4); var leftX = ("P1 LStick X", (int)float.Parse(leftXRaw)); var leftY = ("P1 LStick Y", (int)float.Parse(leftYRaw)); var rightX = ("P1 RStick X", (int)float.Parse(rightXRaw)); var rightY = ("P1 RStick Y", (int)float.Parse(rightYRaw)); controllers.AcceptNewAxes(new[] { leftX, leftY, rightX, rightY }); } } if (info.Player2Type != OctoshockDll.ePeripheralType.None) { // As L3 and R3 don't exist on a standard gamepad, handle them separately later. Unfortunately // due to the layout, we handle select separately too first. controllers["P2 Select"] = player2Str[0] != '.'; if (info.Player2Type == OctoshockDll.ePeripheralType.DualShock) { controllers["P2 L3"] = player2Str[1] != '.'; controllers["P2 R3"] = player2Str[2] != '.'; } int offSet = info.Player2Type == OctoshockDll.ePeripheralType.Pad ? 0 : 2; for (int button = 1; button < buttons.Length; button++) { controllers[$"P2 {buttons[button]}"] = player2Str[button + offSet] != '.'; } if (info.Player2Type == OctoshockDll.ePeripheralType.DualShock) { // The analog controls are encoded as four space-separated numbers with a leading space string leftXRaw = player2Str.Substring(16, 4); string leftYRaw = player2Str.Substring(20, 4); string rightXRaw = player2Str.Substring(24, 4); string rightYRaw = player2Str.Substring(28, 4); var leftX = ("P2 LStick X", (int)float.Parse(leftXRaw)); var leftY = ("P2 LStick Y", (int)float.Parse(leftYRaw)); var rightX = ("P2 RStick X", (int)float.Parse(rightXRaw)); var rightY = ("P2 RStick Y", (int)float.Parse(rightYRaw)); controllers.AcceptNewAxes(new[] { leftX, leftY, rightX, rightY }); } } byte controlState = (byte)controlStr[0]; controllers["Reset"] = (controlState & 0x02) != 0; if ((controlState & 0x04) != 0) { if (isCdTrayOpen) { controllers["Close"] = true; cdNumber++; } else { controllers["Open"] = true; } isCdTrayOpen = !isCdTrayOpen; } else { controllers["Close"] = false; controllers["Open"] = false; } var discSelect = ("Disc Select", cdNumber); controllers.AcceptNewAxes(new[] { discSelect }); if ((controlState & 0xFC) != 0) { Result.Warnings.Add($"Ignored toggle hack flag on frame {frame}"); } movie.AppendFrame(controllers); } }
// GMV file format: http://code.google.com/p/gens-rerecording/wiki/GMV private static BkmMovie ImportGMV(string path, out string errorMsg, out string warningMsg) { errorMsg = warningMsg = string.Empty; BkmMovie m = new BkmMovie(path); FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read); BinaryReader r = new BinaryReader(fs); // 000 16-byte signature and format version: "Gens Movie TEST9" string signature = r.ReadStringFixedAscii(15); if (signature != "Gens Movie TEST") { errorMsg = "This is not a valid .GMV file."; r.Close(); fs.Close(); return null; } m.Header[HeaderKeys.PLATFORM] = "GEN"; // 00F ASCII-encoded GMV file format version. The most recent is 'A'. (?) string version = r.ReadStringFixedAscii(1); m.Comments.Add(MOVIEORIGIN + " .GMV version " + version); m.Comments.Add(EMULATIONORIGIN + " Gens"); // 010 4-byte little-endian unsigned int: rerecord count uint rerecordCount = r.ReadUInt32(); m.Rerecords = rerecordCount; // 014 ASCII-encoded controller config for player 1. '3' or '6'. string player1Config = r.ReadStringFixedAscii(1); // 015 ASCII-encoded controller config for player 2. '3' or '6'. string player2Config = r.ReadStringFixedAscii(1); SimpleController controllers = new SimpleController { Type = new ControllerDefinition() }; if (player1Config == "6" || player2Config == "6") { controllers.Type.Name = "GPGX Genesis Controller"; } else { controllers.Type.Name = "GPGX 3-Button Controller"; } // 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); m.Header[HeaderKeys.PAL] = pal.ToString(); // bit 6: if "1", movie requires a savestate. if (((flags >> 6) & 0x1) != 0) { errorMsg = "Movies that begin with a savestate are not supported."; r.Close(); fs.Close(); return null; } // bit 5: if "1", movie is 3-player movie; if "0", movie is 2-player movie bool threePlayers = (((flags >> 5) & 0x1) != 0); // Unknown. r.ReadByte(); // 018 40-byte zero-terminated ASCII movie name string string description = NullTerminated(r.ReadStringFixedAscii(40)); m.Comments.Add(COMMENT + " " + 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 <filesize minus 64> 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++) { controllers["P" + player + " " + buttons[button]] = (((controllerState >> button) & 0x1) == 0); } } else { for (int button = 0; button < other.Length; button++) { if (player1Config == "6") { controllers["P1 " + other[button]] = (((controllerState >> button) & 0x1) == 0); } if (player2Config == "6") { controllers["P2 " + other[button]] = (((controllerState >> (button + 4)) & 0x1) == 0); } } } } m.AppendFrame(controllers); } return m; }
protected void parseBinaryInputLog(BinaryReader br, Bk2Movie movie, MiscHeaderInfo info) { Octoshock.SyncSettings settings = new Octoshock.SyncSettings(); SimpleController controllers = new SimpleController(); settings.FIOConfig.Devices8 = new[] { info.player1Type, OctoshockDll.ePeripheralType.None,OctoshockDll.ePeripheralType.None,OctoshockDll.ePeripheralType.None, info.player2Type, OctoshockDll.ePeripheralType.None,OctoshockDll.ePeripheralType.None,OctoshockDll.ePeripheralType.None }; controllers.Type = Octoshock.CreateControllerDefinition(settings); string[] buttons = { "Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left", "L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square"}; bool isCdTrayOpen = false; int cdNumber = 1; for (int frame = 0; frame < info.frameCount; ++frame) { if (info.player1Type != OctoshockDll.ePeripheralType.None) { UInt16 controllerState = br.ReadUInt16(); // As L3 and R3 don't exist on a standard gamepad, handle them separately later. Unfortunately // due to the layout, we handle select separately too first. controllers["P1 Select"] = (controllerState & 0x1) != 0; for (int button = 3; button < buttons.Length; button++) { controllers["P1 " + buttons[button]] = (((controllerState >> button) & 0x1) != 0); if (((controllerState >> button) & 0x1) != 0 && button > 15) { continue; } } if (info.player1Type == OctoshockDll.ePeripheralType.DualShock) { controllers["P1 L3"] = (controllerState & 0x2) != 0; controllers["P1 R3"] = (controllerState & 0x4) != 0; Tuple<string, float> leftX = new Tuple<string, float>("P1 LStick X", (float)br.ReadByte()); Tuple<string, float> leftY = new Tuple<string, float>("P1 LStick Y", (float)br.ReadByte()); Tuple<string, float> rightX = new Tuple<string, float>("P1 RStick X", (float)br.ReadByte()); Tuple<string, float> rightY = new Tuple<string, float>("P1 RStick Y", (float)br.ReadByte()); controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY }); } } if (info.player2Type != OctoshockDll.ePeripheralType.None) { UInt16 controllerState = br.ReadUInt16(); for (int button = 0; button < buttons.Length; button++) { controllers["P2 " + buttons[button]] = (((controllerState >> button) & 0x1) != 0); if (((controllerState >> button) & 0x1) != 0 && button > 15) { continue; } } if (info.player2Type == OctoshockDll.ePeripheralType.DualShock) { Tuple<string, float> leftX = new Tuple<string, float>("P2 LStick X", (float)br.ReadByte()); Tuple<string, float> leftY = new Tuple<string, float>("P2 LStick Y", (float)br.ReadByte()); Tuple<string, float> rightX = new Tuple<string, float>("P2 RStick X", (float)br.ReadByte()); Tuple<string, float> rightY = new Tuple<string, float>("P2 RStick Y", (float)br.ReadByte()); controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY }); } } byte controlState = br.ReadByte(); controllers["Reset"] = (controlState & 0x02) != 0; if ((controlState & 0x04) != 0) { if (isCdTrayOpen) { controllers["Close"] = true; cdNumber++; } else { controllers["Open"] = true; } isCdTrayOpen = !isCdTrayOpen; } else { controllers["Close"] = false; controllers["Open"] = false; } Tuple<string, float> discSelect = new Tuple<string, float>("Disc Select", cdNumber); controllers.AcceptNewFloats(new[] { discSelect }); if ((controlState & 0xFC) != 0) { Result.Warnings.Add("Ignored toggle hack flag on frame " + frame.ToString()); } movie.AppendFrame(controllers); } }