1
- using System . IO ;
1
+ using System . Collections . Generic ;
2
+ using System . IO ;
2
3
using System . IO . Compression ;
3
4
using System . Linq ;
4
5
using System . Text ;
5
- using BizHawk . Common ;
6
6
using BizHawk . Common . IOExtensions ;
7
7
using BizHawk . Emulation . Common ;
8
8
using BizHawk . Emulation . Cores ;
9
- using BizHawk . Emulation . Cores . Nintendo . SNES ;
9
+ using BizHawk . Emulation . Cores . Nintendo . BSNES ;
10
10
11
11
namespace BizHawk . Client . Common . movie . import
12
12
{
@@ -16,10 +16,14 @@ namespace BizHawk.Client.Common.movie.import
16
16
internal class LsmvImport : MovieImporter
17
17
{
18
18
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
+
20
24
protected override void RunImport ( )
21
25
{
22
- Result . Movie . HeaderEntries [ HeaderKeys . Core ] = CoreNames . Bsnes ;
26
+ Result . Movie . HeaderEntries [ HeaderKeys . Core ] = CoreNames . SubBsnes115 ;
23
27
24
28
// .LSMV movies are .zip files containing data files.
25
29
using var fs = new FileStream ( SourceFile . FullName , FileMode . Open , FileAccess . Read ) ;
@@ -36,15 +40,48 @@ protected override void RunImport()
36
40
37
41
using var zip = new ZipArchive ( fs , ZipArchiveMode . Read , true ) ;
38
42
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 ( ) ;
45
44
46
45
string platform = VSystemID . Raw . SNES ;
47
46
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
+
48
85
foreach ( var item in zip . Entries )
49
86
{
50
87
if ( item . FullName == "authors" )
@@ -55,10 +92,8 @@ protected override void RunImport()
55
92
string authorLast = "" ;
56
93
using ( var reader = new StringReader ( authors ) )
57
94
{
58
- string line ;
59
-
60
95
// Each author is on a different line.
61
- while ( ( line = reader . ReadLine ( ) ) != null )
96
+ while ( reader . ReadLine ( ) is string line )
62
97
{
63
98
string author = line . Trim ( ) ;
64
99
if ( author != "" )
@@ -127,34 +162,19 @@ protected override void RunImport()
127
162
using var stream = item . Open ( ) ;
128
163
string input = Encoding . UTF8 . GetString ( stream . ReadAllBytes ( ) ) ;
129
164
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 ( ) ) ;
131
168
using ( var reader = new StringReader ( input ) )
132
169
{
133
- lineNum ++ ;
134
- string line ;
135
- while ( ( line = reader . ReadLine ( ) ) != null )
170
+ while ( reader . ReadLine ( ) is string line )
136
171
{
137
- if ( line == "" )
138
- {
139
- continue ;
140
- }
172
+ if ( line == "" ) continue ;
141
173
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 ) ;
156
175
}
157
176
}
177
+ Result . Movie . AppendFrame ( _previousControllers ) ;
158
178
}
159
179
else if ( item . FullName . StartsWith ( "moviesram." ) )
160
180
{
@@ -167,22 +187,6 @@ protected override void RunImport()
167
187
return ;
168
188
}
169
189
}
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
- }
186
190
else if ( item . FullName == "projectid" )
187
191
{
188
192
using var stream = item . Open ( ) ;
@@ -193,19 +197,19 @@ protected override void RunImport()
193
197
{
194
198
using var stream = item . Open ( ) ;
195
199
string rerecords = Encoding . UTF8 . GetString ( stream . ReadAllBytes ( ) ) ;
196
- int rerecordCount ;
200
+ ulong rerecordCount ;
197
201
198
202
// Try to parse the re-record count as an integer, defaulting to 0 if it fails.
199
203
try
200
204
{
201
- rerecordCount = int . Parse ( rerecords ) ;
205
+ rerecordCount = ulong . Parse ( rerecords ) ;
202
206
}
203
207
catch
204
208
{
205
209
rerecordCount = 0 ;
206
210
}
207
211
208
- Result . Movie . Rerecords = ( ulong ) rerecordCount ;
212
+ Result . Movie . Rerecords = rerecordCount ;
209
213
}
210
214
else if ( item . FullName . EndsWith ( ".sha256" ) )
211
215
{
@@ -226,8 +230,7 @@ protected override void RunImport()
226
230
string subtitles = Encoding . UTF8 . GetString ( stream . ReadAllBytes ( ) ) ;
227
231
using ( var reader = new StringReader ( subtitles ) )
228
232
{
229
- string line ;
230
- while ( ( line = reader . ReadLine ( ) ) != null )
233
+ while ( reader . ReadLine ( ) is string line )
231
234
{
232
235
var subtitle = ImportTextSubtitle ( line ) ;
233
236
if ( ! string . IsNullOrEmpty ( subtitle ) )
@@ -259,12 +262,11 @@ protected override void RunImport()
259
262
260
263
Result . Movie . HeaderEntries [ HeaderKeys . Platform ] = platform ;
261
264
Result . Movie . SyncSettingsJson = ConfigService . SaveWithType ( ss ) ;
262
- MaybeSetCorePreference ( VSystemID . Raw . SNES , CoreNames . Bsnes , fileExt : ".lsmv" ) ;
263
265
}
264
266
265
267
private IController EmptyLmsvFrame ( )
266
268
{
267
- SimpleController emptyController = new ( _deck . Definition ) ;
269
+ SimpleController emptyController = new ( _controllers . Definition ) ;
268
270
269
271
foreach ( var button in emptyController . Definition . BoolButtons )
270
272
{
@@ -274,38 +276,35 @@ private IController EmptyLmsvFrame()
274
276
return emptyController ;
275
277
}
276
278
277
- private void ImportTextFrame ( string line , string platform )
279
+ private void ImportTextFrame ( string line )
278
280
{
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 ) ;
290
282
291
283
// Split up the sections of the frame.
292
284
string [ ] sections = line . Split ( '|' ) ;
293
285
286
+ bool reset = false ;
294
287
if ( sections . Length != 0 )
295
288
{
296
289
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
299
296
{
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 ;
302
302
}
303
303
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 )
307
305
{
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
309
308
}
310
309
311
310
controllers [ "Reset" ] = reset ;
@@ -316,28 +315,33 @@ private void ImportTextFrame(string line, string platform)
316
315
317
316
for ( int player = 1 ; player < end ; player ++ )
318
317
{
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
323
322
{
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 ( ) ;
325
326
}
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 )
330
329
{
331
- for ( int button = 0 ; button < buttons . Length ; button ++ )
330
+ for ( int button = 0 ; button < buttons . Count ; button ++ )
332
331
{
333
332
// 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 ] != '.' ;
335
334
}
336
335
}
337
336
}
338
337
339
338
// 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 ;
341
345
}
342
346
343
347
private static string ImportTextSubtitle ( string line )
0 commit comments