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

395 lines
11 KiB
C#

using System;
using System.IO;
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;
namespace BizHawk.Client.Common.movie.import
{
// ReSharper disable once UnusedMember.Global
// LSMV file format: http://tasvideos.org/Lsnes/Movieformat.html
[ImporterFor("LSNES", ".lsmv")]
internal class LsmvImport : MovieImporter
{
private LibsnesControllerDeck _deck;
protected override void RunImport()
{
var bsnesName = ((CoreAttribute)Attribute.GetCustomAttribute(typeof(LibsnesCore), typeof(CoreAttribute))).CoreName;
Result.Movie.HeaderEntries[HeaderKeys.Core] = bsnesName;
var hf = new HawkFile(SourceFile.FullName);
// .LSMV movies are .zip files containing data files.
if (!hf.IsArchive)
{
Result.Errors.Add("This is not an archive.");
return;
}
var ss = new LibsnesCore.SnesSyncSettings
{
LeftPort = LibsnesControllerDeck.ControllerType.Gamepad,
RightPort = LibsnesControllerDeck.ControllerType.Gamepad
};
_deck = new LibsnesControllerDeck(ss);
string platform = "SNES";
foreach (var item in hf.ArchiveItems)
{
if (item.Name == "authors")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string authors = Encoding.UTF8.GetString(stream.ReadAllBytes());
string authorList = "";
string authorLast = "";
using (var reader = new StringReader(authors))
{
string line;
// Each author is on a different line.
while ((line = reader.ReadLine()) != null)
{
string author = line.Trim();
if (author != "")
{
if (authorLast != "")
{
authorList += $"{authorLast}, ";
}
authorLast = author;
}
}
}
if (authorList != "")
{
authorList += "and ";
}
if (authorLast != "")
{
authorList += authorLast;
}
Result.Movie.HeaderEntries[HeaderKeys.Author] = authorList;
hf.Unbind();
}
else if (item.Name == "coreversion")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string coreVersion = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.Comments.Add($"CoreOrigin {coreVersion}");
hf.Unbind();
}
else if (item.Name == "gamename")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string gameName = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries[HeaderKeys.GameName] = gameName;
hf.Unbind();
}
else if (item.Name == "gametype")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string gametype = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
// TODO: Handle the other types.
switch (gametype)
{
case "gdmg":
platform = "GB";
break;
case "ggbc":
case "ggbca":
platform = "GBC";
break;
case "sgb_ntsc":
case "sgb_pal":
platform = "SNES";
Config.GbAsSgb = true;
break;
}
bool pal = gametype == "snes_pal" || gametype == "sgb_pal";
Result.Movie.HeaderEntries[HeaderKeys.Pal] = pal.ToString();
hf.Unbind();
}
else if (item.Name == "input")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string input = Encoding.UTF8.GetString(stream.ReadAllBytes());
int lineNum = 0;
using (var reader = new StringReader(input))
{
lineNum++;
string line;
while ((line = reader.ReadLine()) != null)
{
if (line == "")
{
continue;
}
// Insert an empty frame in lsmv snes movies
// https://github.com/TASVideos/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 == "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);
}
}
hf.Unbind();
}
else if (item.Name.StartsWith("moviesram."))
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
byte[] movieSram = stream.ReadAllBytes();
if (movieSram.Length != 0)
{
Result.Errors.Add("Movies that begin with SRAM are not supported.");
hf.Unbind();
return;
}
}
else if (item.Name == "port1")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string port1 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries["port1"] = port1;
ss.LeftPort = LibsnesControllerDeck.ControllerType.Gamepad;
_deck = new LibsnesControllerDeck(ss);
hf.Unbind();
}
else if (item.Name == "port2")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string port2 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries["port2"] = port2;
ss.RightPort = LibsnesControllerDeck.ControllerType.Gamepad;
_deck = new LibsnesControllerDeck(ss);
hf.Unbind();
}
else if (item.Name == "projectid")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string projectId = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries["ProjectID"] = projectId;
hf.Unbind();
}
else if (item.Name == "rerecords")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string rerecords = Encoding.UTF8.GetString(stream.ReadAllBytes());
int rerecordCount;
// Try to parse the re-record count as an integer, defaulting to 0 if it fails.
try
{
rerecordCount = int.Parse(rerecords);
}
catch
{
rerecordCount = 0;
}
Result.Movie.Rerecords = (ulong)rerecordCount;
hf.Unbind();
}
else if (item.Name.EndsWith(".sha256"))
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string rom = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
int pos = item.Name.LastIndexOf(".sha256");
string name = item.Name.Substring(0, pos);
Result.Movie.HeaderEntries[$"SHA256_{name}"] = rom;
hf.Unbind();
}
else if (item.Name == "savestate")
{
Result.Errors.Add("Movies that begin with a savestate are not supported.");
return;
}
else if (item.Name == "subtitles")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string subtitles = Encoding.UTF8.GetString(stream.ReadAllBytes());
using (var reader = new StringReader(subtitles))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var subtitle = ImportTextSubtitle(line);
if (!string.IsNullOrEmpty(subtitle))
{
Result.Movie.Subtitles.AddFromString(subtitle);
}
}
}
hf.Unbind();
}
else if (item.Name == "starttime.second")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string startSecond = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries["StartSecond"] = startSecond;
hf.Unbind();
}
else if (item.Name == "starttime.subsecond")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string startSubSecond = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.HeaderEntries["StartSubSecond"] = startSubSecond;
hf.Unbind();
}
else if (item.Name == "systemid")
{
hf.BindArchiveMember(item.Index);
var stream = hf.GetStream();
string systemId = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
Result.Movie.Comments.Add($"{EmulationOrigin} {systemId}");
hf.Unbind();
}
}
Result.Movie.HeaderEntries[HeaderKeys.Platform] = platform;
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss);
Config.PreferredCores["SNES"] = CoreNames.Bsnes; // TODO: convert to snes9x if it is the user's preference
}
private IController EmptyLmsvFrame()
{
var emptyController = new SimpleController
{
Definition = _deck.Definition
};
foreach (var button in emptyController.Definition.BoolButtons)
{
emptyController[button] = false;
}
return emptyController;
}
private void ImportTextFrame(string line, string platform)
{
var controllers = new SimpleController
{
Definition = _deck.Definition
};
var buttons = new[]
{
"B", "Y", "Select", "Start", "Up", "Down", "Left", "Right", "A", "X", "L", "R"
};
if (platform == "GB" || platform == "GBC")
{
buttons = new[] { "A", "B", "Select", "Start", "Right", "Left", "Up", "Down" };
}
// Split up the sections of the frame.
string[] sections = line.Split('|');
if (sections.Length != 0)
{
string flags = sections[0];
char[] off = { '.', ' ', '\t', '\n', '\r' };
if (flags.Length == 0 || off.Contains(flags[0]))
{
Result.Warnings.Add("Unable to import subframe.");
}
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')))
{
Result.Warnings.Add("Unable to import delayed reset.");
}
controllers["Reset"] = reset;
}
// LSNES frames don't start or end with a |.
int end = sections.Length;
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 = "";
}
// 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] != '.';
}
}
}
// Convert the data for the controllers to a mnemonic and add it as a frame.
Result.Movie.AppendFrame(controllers);
}
private static string ImportTextSubtitle(string line)
{
line = SingleSpaces(line);
// The header name, frame, and message are separated by whitespace.
int first = line.IndexOf(' ');
int second = line.IndexOf(' ', first + 1);
if (first != -1 && second != -1)
{
// Concatenate the frame and message with default values for the additional fields.
string frame = line.Substring(0, first);
string length = line.Substring(first + 1, second - first - 1);
string message = line.Substring(second + 1).Trim();
return $"subtitle {frame} 0 0 {length} FFFFFFFF {message}";
}
return null;
}
}
}