Improve LsmvImport in numerous ways

- previously, every second frame was empty (lol), since 2016 i believe
- now imports as (sub)bsnes115 movie instead of bsnes, allowing to import subframe inputs and delayed resets
- imports controller types correct(er)ly
This commit is contained in:
Morilli 2022-12-05 16:26:49 +01:00
parent 10ba45d462
commit 4a49fc174b
1 changed files with 96 additions and 92 deletions

View File

@ -1,12 +1,12 @@
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using BizHawk.Common;
using BizHawk.Common.IOExtensions;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores;
using BizHawk.Emulation.Cores.Nintendo.SNES;
using BizHawk.Emulation.Cores.Nintendo.BSNES;
namespace BizHawk.Client.Common.movie.import
{
@ -16,10 +16,14 @@ namespace BizHawk.Client.Common.movie.import
internal class LsmvImport : MovieImporter
{
private static readonly byte[] Zipheader = { 0x50, 0x4b, 0x03, 0x04 };
private LibsnesControllerDeck _deck;
private BsnesControllers _controllers;
private int _playerCount;
// hacky variable; just exists because if subframe input is used, the previous frame needs to be marked subframe aware
private SimpleController _previousControllers;
protected override void RunImport()
{
Result.Movie.HeaderEntries[HeaderKeys.Core] = CoreNames.Bsnes;
Result.Movie.HeaderEntries[HeaderKeys.Core] = CoreNames.SubBsnes115;
// .LSMV movies are .zip files containing data files.
using var fs = new FileStream(SourceFile.FullName, FileMode.Open, FileAccess.Read);
@ -36,15 +40,48 @@ namespace BizHawk.Client.Common.movie.import
using var zip = new ZipArchive(fs, ZipArchiveMode.Read, true);
var ss = new LibsnesCore.SnesSyncSettings
{
LeftPort = LibsnesControllerDeck.ControllerType.Gamepad,
RightPort = LibsnesControllerDeck.ControllerType.Gamepad
};
_deck = new LibsnesControllerDeck(ss);
var ss = new BsnesCore.SnesSyncSettings();
string platform = VSystemID.Raw.SNES;
// need to handle ports first to ensure controller types are known
ZipArchiveEntry portEntry;
if ((portEntry = zip.GetEntry("port1")) != null)
{
using var stream = portEntry.Open();
string port1 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries["port1"] = port1;
ss.LeftPort = port1 switch
{
"none" => BsnesApi.BSNES_PORT1_INPUT_DEVICE.None,
// "gamepad16" => BsnesApi.BSNES_PORT1_INPUT_DEVICE.ExtendedGamepad, // coming soon (hopefully)
"multitap" => BsnesApi.BSNES_PORT1_INPUT_DEVICE.SuperMultitap,
"multitap16" => BsnesApi.BSNES_PORT1_INPUT_DEVICE.Payload,
_ => BsnesApi.BSNES_PORT1_INPUT_DEVICE.Gamepad
};
}
if ((portEntry = zip.GetEntry("port2")) != null)
{
using var stream = portEntry.Open();
string port2 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries["port2"] = port2;
ss.RightPort = port2 switch
{
"none" => BsnesApi.BSNES_INPUT_DEVICE.None,
// "gamepad16" => BsnesApi.BSNES_INPUT_DEVICE.ExtendedGamepad, // coming soon (hopefully)
"multitap" => BsnesApi.BSNES_INPUT_DEVICE.SuperMultitap,
"multitap16" => BsnesApi.BSNES_INPUT_DEVICE.Payload,
// will these even work lol
"superscope" => BsnesApi.BSNES_INPUT_DEVICE.SuperScope,
"justifier" => BsnesApi.BSNES_INPUT_DEVICE.Justifier,
"justifiers" => BsnesApi.BSNES_INPUT_DEVICE.Justifiers,
_ => BsnesApi.BSNES_INPUT_DEVICE.Gamepad
};
}
_controllers = new BsnesControllers(ss, true);
Result.Movie.LogKey = new Bk2LogEntryGenerator("SNES", new Bk2Controller(_controllers.Definition)).GenerateLogKey();
_playerCount = _controllers.Definition.PlayerCount;
foreach (var item in zip.Entries)
{
if (item.FullName == "authors")
@ -55,10 +92,8 @@ namespace BizHawk.Client.Common.movie.import
string authorLast = "";
using (var reader = new StringReader(authors))
{
string line;
// Each author is on a different line.
while ((line = reader.ReadLine()) != null)
while (reader.ReadLine() is string line)
{
string author = line.Trim();
if (author != "")
@ -127,34 +162,19 @@ namespace BizHawk.Client.Common.movie.import
using var stream = item.Open();
string input = Encoding.UTF8.GetString(stream.ReadAllBytes());
int lineNum = 0;
// Insert an empty frame in lsmv snes movies
// see https://github.com/TASEmulators/BizHawk/issues/721
Result.Movie.AppendFrame(EmptyLmsvFrame());
using (var reader = new StringReader(input))
{
lineNum++;
string line;
while ((line = reader.ReadLine()) != null)
while(reader.ReadLine() is string line)
{
if (line == "")
{
continue;
}
if (line == "") continue;
// Insert an empty frame in lsmv snes movies
// https://github.com/TASEmulators/BizHawk/issues/721
// Both emulators send the input to bsnes core at the same V interval, but:
// lsnes' frame boundary occurs at V = 241, after which the input is read;
// BizHawk's frame boundary is just before automatic polling;
// This isn't a great place to add this logic but this code is a mess
if (lineNum == 1 && platform == VSystemID.Raw.SNES)
{
// Note that this logic assumes the first non-empty log entry is a valid input log entry
// and that it is NOT a subframe input entry. It seems safe to assume subframe input would not be on the first line
Result.Movie.AppendFrame(EmptyLmsvFrame());
}
ImportTextFrame(line, platform);
ImportTextFrame(line);
}
}
Result.Movie.AppendFrame(_previousControllers);
}
else if (item.FullName.StartsWith("moviesram."))
{
@ -167,22 +187,6 @@ namespace BizHawk.Client.Common.movie.import
return;
}
}
else if (item.FullName == "port1")
{
using var stream = item.Open();
string port1 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries["port1"] = port1;
ss.LeftPort = LibsnesControllerDeck.ControllerType.Gamepad;
_deck = new LibsnesControllerDeck(ss);
}
else if (item.FullName == "port2")
{
using var stream = item.Open();
string port2 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries["port2"] = port2;
ss.RightPort = LibsnesControllerDeck.ControllerType.Gamepad;
_deck = new LibsnesControllerDeck(ss);
}
else if (item.FullName == "projectid")
{
using var stream = item.Open();
@ -193,19 +197,19 @@ namespace BizHawk.Client.Common.movie.import
{
using var stream = item.Open();
string rerecords = Encoding.UTF8.GetString(stream.ReadAllBytes());
int rerecordCount;
ulong rerecordCount;
// Try to parse the re-record count as an integer, defaulting to 0 if it fails.
try
{
rerecordCount = int.Parse(rerecords);
rerecordCount = ulong.Parse(rerecords);
}
catch
{
rerecordCount = 0;
}
Result.Movie.Rerecords = (ulong)rerecordCount;
Result.Movie.Rerecords = rerecordCount;
}
else if (item.FullName.EndsWith(".sha256"))
{
@ -226,8 +230,7 @@ namespace BizHawk.Client.Common.movie.import
string subtitles = Encoding.UTF8.GetString(stream.ReadAllBytes());
using (var reader = new StringReader(subtitles))
{
string line;
while ((line = reader.ReadLine()) != null)
while (reader.ReadLine() is string line)
{
var subtitle = ImportTextSubtitle(line);
if (!string.IsNullOrEmpty(subtitle))
@ -259,12 +262,11 @@ namespace BizHawk.Client.Common.movie.import
Result.Movie.HeaderEntries[HeaderKeys.Platform] = platform;
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss);
MaybeSetCorePreference(VSystemID.Raw.SNES, CoreNames.Bsnes, fileExt: ".lsmv");
}
private IController EmptyLmsvFrame()
{
SimpleController emptyController = new(_deck.Definition);
SimpleController emptyController = new(_controllers.Definition);
foreach (var button in emptyController.Definition.BoolButtons)
{
@ -274,38 +276,35 @@ namespace BizHawk.Client.Common.movie.import
return emptyController;
}
private void ImportTextFrame(string line, string platform)
private void ImportTextFrame(string line)
{
SimpleController controllers = new(_deck.Definition);
var buttons = new[]
{
"B", "Y", "Select", "Start", "Up", "Down", "Left", "Right", "A", "X", "L", "R"
};
if (platform == VSystemID.Raw.GB || platform == VSystemID.Raw.GBC)
{
buttons = new[] { "A", "B", "Select", "Start", "Right", "Left", "Up", "Down" };
}
SimpleController controllers = new(_controllers.Definition);
// Split up the sections of the frame.
string[] sections = line.Split('|');
bool reset = false;
if (sections.Length != 0)
{
string flags = sections[0];
char[] off = { '.', ' ', '\t', '\n', '\r' };
if (flags.Length == 0 || off.Contains(flags[0]))
if (flags[0] != 'F' && _previousControllers != null) _previousControllers["Subframe"] = true;
reset = flags[1] != '.';
flags = SingleSpaces(flags.Substring(2));
string[] splitFlags = flags.Split(' ');
int delay;
try
{
Result.Warnings.Add("Unable to import subframe.");
delay = int.Parse(splitFlags[1]) * 10000 + int.Parse(splitFlags[2]);
}
catch
{
delay = 0;
}
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 (delay != 0)
{
Result.Warnings.Add("Unable to import delayed reset.");
controllers.AcceptNewAxis("Reset Instruction", delay);
Result.Warnings.Add("Delayed reset may be mistimed."); // lsnes doesn't count some instructions that our bsnes version does
}
controllers["Reset"] = reset;
@ -316,28 +315,33 @@ namespace BizHawk.Client.Common.movie.import
for (int player = 1; player < end; player++)
{
string prefix = $"P{player} ";
// Gameboy doesn't currently have a prefix saying which player the input is for.
if (controllers.Definition.Name == "Gameboy Controller")
{
prefix = "";
}
if (player > _playerCount) break;
// Only count lines with that have the right number of buttons and are for valid players.
if (
sections[player].Length == buttons.Length)
IReadOnlyList<string> buttons = controllers.Definition.ControlsOrdered[player];
if (buttons[0].EndsWith("Up")) // hack to identify gamepad / multitap which have a different button order in bizhawk compared to lsnes
{
for (int button = 0; button < buttons.Length; button++)
buttons = new[] { "B", "Y", "Select", "Start", "Up", "Down", "Left", "Right", "A", "X", "L", "R" }
.Select(button => $"P{player} {button}")
.ToList();
}
// Only consider lines that have the right number of buttons
if (sections[player].Length == buttons.Count)
{
for (int button = 0; button < buttons.Count; button++)
{
// Consider the button pressed so long as its spot is not occupied by a ".".
controllers[prefix + buttons[button]] = sections[player][button] != '.';
controllers[buttons[button]] = sections[player][button] != '.';
}
}
}
// Convert the data for the controllers to a mnemonic and add it as a frame.
Result.Movie.AppendFrame(controllers);
if (_previousControllers != null)
Result.Movie.AppendFrame(_previousControllers);
if (reset) Result.Movie.AppendFrame(EmptyLmsvFrame());
_previousControllers = controllers;
}
private static string ImportTextSubtitle(string line)