Skip to content

Commit 4a49fc1

Browse files
committed
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
1 parent 10ba45d commit 4a49fc1

File tree

1 file changed

+95
-91
lines changed

1 file changed

+95
-91
lines changed

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

Lines changed: 95 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
using System.IO;
1+
using System.Collections.Generic;
2+
using System.IO;
23
using System.IO.Compression;
34
using System.Linq;
45
using System.Text;
5-
using BizHawk.Common;
66
using BizHawk.Common.IOExtensions;
77
using BizHawk.Emulation.Common;
88
using BizHawk.Emulation.Cores;
9-
using BizHawk.Emulation.Cores.Nintendo.SNES;
9+
using BizHawk.Emulation.Cores.Nintendo.BSNES;
1010

1111
namespace BizHawk.Client.Common.movie.import
1212
{
@@ -16,10 +16,14 @@ namespace BizHawk.Client.Common.movie.import
1616
internal class LsmvImport : MovieImporter
1717
{
1818
private static readonly byte[] Zipheader = { 0x50, 0x4b, 0x03, 0x04 };
19-
private LibsnesControllerDeck _deck;
19+
private BsnesControllers _controllers;
20+
private int _playerCount;
21+
// hacky variable; just exists because if subframe input is used, the previous frame needs to be marked subframe aware
22+
private SimpleController _previousControllers;
23+
2024
protected override void RunImport()
2125
{
22-
Result.Movie.HeaderEntries[HeaderKeys.Core] = CoreNames.Bsnes;
26+
Result.Movie.HeaderEntries[HeaderKeys.Core] = CoreNames.SubBsnes115;
2327

2428
// .LSMV movies are .zip files containing data files.
2529
using var fs = new FileStream(SourceFile.FullName, FileMode.Open, FileAccess.Read);
@@ -36,15 +40,48 @@ protected override void RunImport()
3640

3741
using var zip = new ZipArchive(fs, ZipArchiveMode.Read, true);
3842

39-
var ss = new LibsnesCore.SnesSyncSettings
40-
{
41-
LeftPort = LibsnesControllerDeck.ControllerType.Gamepad,
42-
RightPort = LibsnesControllerDeck.ControllerType.Gamepad
43-
};
44-
_deck = new LibsnesControllerDeck(ss);
43+
var ss = new BsnesCore.SnesSyncSettings();
4544

4645
string platform = VSystemID.Raw.SNES;
4746

47+
// need to handle ports first to ensure controller types are known
48+
ZipArchiveEntry portEntry;
49+
if ((portEntry = zip.GetEntry("port1")) != null)
50+
{
51+
using var stream = portEntry.Open();
52+
string port1 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
53+
Result.Movie.HeaderEntries["port1"] = port1;
54+
ss.LeftPort = port1 switch
55+
{
56+
"none" => BsnesApi.BSNES_PORT1_INPUT_DEVICE.None,
57+
// "gamepad16" => BsnesApi.BSNES_PORT1_INPUT_DEVICE.ExtendedGamepad, // coming soon (hopefully)
58+
"multitap" => BsnesApi.BSNES_PORT1_INPUT_DEVICE.SuperMultitap,
59+
"multitap16" => BsnesApi.BSNES_PORT1_INPUT_DEVICE.Payload,
60+
_ => BsnesApi.BSNES_PORT1_INPUT_DEVICE.Gamepad
61+
};
62+
}
63+
if ((portEntry = zip.GetEntry("port2")) != null)
64+
{
65+
using var stream = portEntry.Open();
66+
string port2 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
67+
Result.Movie.HeaderEntries["port2"] = port2;
68+
ss.RightPort = port2 switch
69+
{
70+
"none" => BsnesApi.BSNES_INPUT_DEVICE.None,
71+
// "gamepad16" => BsnesApi.BSNES_INPUT_DEVICE.ExtendedGamepad, // coming soon (hopefully)
72+
"multitap" => BsnesApi.BSNES_INPUT_DEVICE.SuperMultitap,
73+
"multitap16" => BsnesApi.BSNES_INPUT_DEVICE.Payload,
74+
// will these even work lol
75+
"superscope" => BsnesApi.BSNES_INPUT_DEVICE.SuperScope,
76+
"justifier" => BsnesApi.BSNES_INPUT_DEVICE.Justifier,
77+
"justifiers" => BsnesApi.BSNES_INPUT_DEVICE.Justifiers,
78+
_ => BsnesApi.BSNES_INPUT_DEVICE.Gamepad
79+
};
80+
}
81+
_controllers = new BsnesControllers(ss, true);
82+
Result.Movie.LogKey = new Bk2LogEntryGenerator("SNES", new Bk2Controller(_controllers.Definition)).GenerateLogKey();
83+
_playerCount = _controllers.Definition.PlayerCount;
84+
4885
foreach (var item in zip.Entries)
4986
{
5087
if (item.FullName == "authors")
@@ -55,10 +92,8 @@ protected override void RunImport()
5592
string authorLast = "";
5693
using (var reader = new StringReader(authors))
5794
{
58-
string line;
59-
6095
// Each author is on a different line.
61-
while ((line = reader.ReadLine()) != null)
96+
while (reader.ReadLine() is string line)
6297
{
6398
string author = line.Trim();
6499
if (author != "")
@@ -127,34 +162,19 @@ protected override void RunImport()
127162
using var stream = item.Open();
128163
string input = Encoding.UTF8.GetString(stream.ReadAllBytes());
129164

130-
int lineNum = 0;
165+
// Insert an empty frame in lsmv snes movies
166+
// see https://github.com/TASEmulators/BizHawk/issues/721
167+
Result.Movie.AppendFrame(EmptyLmsvFrame());
131168
using (var reader = new StringReader(input))
132169
{
133-
lineNum++;
134-
string line;
135-
while ((line = reader.ReadLine()) != null)
170+
while(reader.ReadLine() is string line)
136171
{
137-
if (line == "")
138-
{
139-
continue;
140-
}
172+
if (line == "") continue;
141173

142-
// Insert an empty frame in lsmv snes movies
143-
// https://github.com/TASEmulators/BizHawk/issues/721
144-
// Both emulators send the input to bsnes core at the same V interval, but:
145-
// lsnes' frame boundary occurs at V = 241, after which the input is read;
146-
// BizHawk's frame boundary is just before automatic polling;
147-
// This isn't a great place to add this logic but this code is a mess
148-
if (lineNum == 1 && platform == VSystemID.Raw.SNES)
149-
{
150-
// Note that this logic assumes the first non-empty log entry is a valid input log entry
151-
// and that it is NOT a subframe input entry. It seems safe to assume subframe input would not be on the first line
152-
Result.Movie.AppendFrame(EmptyLmsvFrame());
153-
}
154-
155-
ImportTextFrame(line, platform);
174+
ImportTextFrame(line);
156175
}
157176
}
177+
Result.Movie.AppendFrame(_previousControllers);
158178
}
159179
else if (item.FullName.StartsWith("moviesram."))
160180
{
@@ -167,22 +187,6 @@ protected override void RunImport()
167187
return;
168188
}
169189
}
170-
else if (item.FullName == "port1")
171-
{
172-
using var stream = item.Open();
173-
string port1 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
174-
Result.Movie.HeaderEntries["port1"] = port1;
175-
ss.LeftPort = LibsnesControllerDeck.ControllerType.Gamepad;
176-
_deck = new LibsnesControllerDeck(ss);
177-
}
178-
else if (item.FullName == "port2")
179-
{
180-
using var stream = item.Open();
181-
string port2 = Encoding.UTF8.GetString(stream.ReadAllBytes()).Trim();
182-
Result.Movie.HeaderEntries["port2"] = port2;
183-
ss.RightPort = LibsnesControllerDeck.ControllerType.Gamepad;
184-
_deck = new LibsnesControllerDeck(ss);
185-
}
186190
else if (item.FullName == "projectid")
187191
{
188192
using var stream = item.Open();
@@ -193,19 +197,19 @@ protected override void RunImport()
193197
{
194198
using var stream = item.Open();
195199
string rerecords = Encoding.UTF8.GetString(stream.ReadAllBytes());
196-
int rerecordCount;
200+
ulong rerecordCount;
197201

198202
// Try to parse the re-record count as an integer, defaulting to 0 if it fails.
199203
try
200204
{
201-
rerecordCount = int.Parse(rerecords);
205+
rerecordCount = ulong.Parse(rerecords);
202206
}
203207
catch
204208
{
205209
rerecordCount = 0;
206210
}
207211

208-
Result.Movie.Rerecords = (ulong)rerecordCount;
212+
Result.Movie.Rerecords = rerecordCount;
209213
}
210214
else if (item.FullName.EndsWith(".sha256"))
211215
{
@@ -226,8 +230,7 @@ protected override void RunImport()
226230
string subtitles = Encoding.UTF8.GetString(stream.ReadAllBytes());
227231
using (var reader = new StringReader(subtitles))
228232
{
229-
string line;
230-
while ((line = reader.ReadLine()) != null)
233+
while (reader.ReadLine() is string line)
231234
{
232235
var subtitle = ImportTextSubtitle(line);
233236
if (!string.IsNullOrEmpty(subtitle))
@@ -259,12 +262,11 @@ protected override void RunImport()
259262

260263
Result.Movie.HeaderEntries[HeaderKeys.Platform] = platform;
261264
Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss);
262-
MaybeSetCorePreference(VSystemID.Raw.SNES, CoreNames.Bsnes, fileExt: ".lsmv");
263265
}
264266

265267
private IController EmptyLmsvFrame()
266268
{
267-
SimpleController emptyController = new(_deck.Definition);
269+
SimpleController emptyController = new(_controllers.Definition);
268270

269271
foreach (var button in emptyController.Definition.BoolButtons)
270272
{
@@ -274,38 +276,35 @@ private IController EmptyLmsvFrame()
274276
return emptyController;
275277
}
276278

277-
private void ImportTextFrame(string line, string platform)
279+
private void ImportTextFrame(string line)
278280
{
279-
SimpleController controllers = new(_deck.Definition);
280-
281-
var buttons = new[]
282-
{
283-
"B", "Y", "Select", "Start", "Up", "Down", "Left", "Right", "A", "X", "L", "R"
284-
};
285-
286-
if (platform == VSystemID.Raw.GB || platform == VSystemID.Raw.GBC)
287-
{
288-
buttons = new[] { "A", "B", "Select", "Start", "Right", "Left", "Up", "Down" };
289-
}
281+
SimpleController controllers = new(_controllers.Definition);
290282

291283
// Split up the sections of the frame.
292284
string[] sections = line.Split('|');
293285

286+
bool reset = false;
294287
if (sections.Length != 0)
295288
{
296289
string flags = sections[0];
297-
char[] off = { '.', ' ', '\t', '\n', '\r' };
298-
if (flags.Length == 0 || off.Contains(flags[0]))
290+
if (flags[0] != 'F' && _previousControllers != null) _previousControllers["Subframe"] = true;
291+
reset = flags[1] != '.';
292+
flags = SingleSpaces(flags.Substring(2));
293+
string[] splitFlags = flags.Split(' ');
294+
int delay;
295+
try
299296
{
300-
Result.Warnings.Add("Unable to import subframe.");
301-
297+
delay = int.Parse(splitFlags[1]) * 10000 + int.Parse(splitFlags[2]);
298+
}
299+
catch
300+
{
301+
delay = 0;
302302
}
303303

304-
bool reset = flags.Length >= 2 && !off.Contains(flags[1]);
305-
flags = SingleSpaces(flags.Substring(2));
306-
if (reset && ((flags.Length >= 2 && flags[1] != '0') || (flags.Length >= 4 && flags[3] != '0')))
304+
if (delay != 0)
307305
{
308-
Result.Warnings.Add("Unable to import delayed reset.");
306+
controllers.AcceptNewAxis("Reset Instruction", delay);
307+
Result.Warnings.Add("Delayed reset may be mistimed."); // lsnes doesn't count some instructions that our bsnes version does
309308
}
310309

311310
controllers["Reset"] = reset;
@@ -316,28 +315,33 @@ private void ImportTextFrame(string line, string platform)
316315

317316
for (int player = 1; player < end; player++)
318317
{
319-
string prefix = $"P{player} ";
320-
321-
// Gameboy doesn't currently have a prefix saying which player the input is for.
322-
if (controllers.Definition.Name == "Gameboy Controller")
318+
if (player > _playerCount) break;
319+
320+
IReadOnlyList<string> buttons = controllers.Definition.ControlsOrdered[player];
321+
if (buttons[0].EndsWith("Up")) // hack to identify gamepad / multitap which have a different button order in bizhawk compared to lsnes
323322
{
324-
prefix = "";
323+
buttons = new[] { "B", "Y", "Select", "Start", "Up", "Down", "Left", "Right", "A", "X", "L", "R" }
324+
.Select(button => $"P{player} {button}")
325+
.ToList();
325326
}
326-
327-
// Only count lines with that have the right number of buttons and are for valid players.
328-
if (
329-
sections[player].Length == buttons.Length)
327+
// Only consider lines that have the right number of buttons
328+
if (sections[player].Length == buttons.Count)
330329
{
331-
for (int button = 0; button < buttons.Length; button++)
330+
for (int button = 0; button < buttons.Count; button++)
332331
{
333332
// Consider the button pressed so long as its spot is not occupied by a ".".
334-
controllers[prefix + buttons[button]] = sections[player][button] != '.';
333+
controllers[buttons[button]] = sections[player][button] != '.';
335334
}
336335
}
337336
}
338337

339338
// Convert the data for the controllers to a mnemonic and add it as a frame.
340-
Result.Movie.AppendFrame(controllers);
339+
if (_previousControllers != null)
340+
Result.Movie.AppendFrame(_previousControllers);
341+
342+
if (reset) Result.Movie.AppendFrame(EmptyLmsvFrame());
343+
344+
_previousControllers = controllers;
341345
}
342346

343347
private static string ImportTextSubtitle(string line)

0 commit comments

Comments
 (0)