src/gba/rr/mgm.c (view raw)
1/* Copyright (c) 2013-2015 Jeffrey Pfau
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6#include "mgm.h"
7
8#include "gba/gba.h"
9#include "gba/serialize.h"
10#include "util/vfs.h"
11
12#define BINARY_EXT ".mgm"
13#define BINARY_MAGIC "GBAb"
14#define METADATA_FILENAME "metadata" BINARY_EXT
15
16enum {
17 INVALID_INPUT = 0x8000
18};
19
20static void GBAMGMContextDestroy(struct GBARRContext*);
21
22static bool GBAMGMStartPlaying(struct GBARRContext*, bool autorecord);
23static void GBAMGMStopPlaying(struct GBARRContext*);
24static bool GBAMGMStartRecording(struct GBARRContext*);
25static void GBAMGMStopRecording(struct GBARRContext*);
26
27static bool GBAMGMIsPlaying(const struct GBARRContext*);
28static bool GBAMGMIsRecording(const struct GBARRContext*);
29
30static void GBAMGMNextFrame(struct GBARRContext*);
31static void GBAMGMLogInput(struct GBARRContext*, uint16_t input);
32static uint16_t GBAMGMQueryInput(struct GBARRContext*);
33static bool GBAMGMQueryReset(struct GBARRContext*);
34
35static void GBAMGMStateSaved(struct GBARRContext* rr, struct GBASerializedState* state);
36static void GBAMGMStateLoaded(struct GBARRContext* rr, const struct GBASerializedState* state);
37
38static bool _loadStream(struct GBAMGMContext*, uint32_t streamId);
39static bool _incrementStream(struct GBAMGMContext*, bool recursive);
40static bool _finishSegment(struct GBAMGMContext*);
41static bool _skipSegment(struct GBAMGMContext*);
42static bool _markRerecord(struct GBAMGMContext*);
43
44static bool _emitMagic(struct GBAMGMContext*, struct VFile* vf);
45static bool _verifyMagic(struct GBAMGMContext*, struct VFile* vf);
46static enum GBAMGMTag _readTag(struct GBAMGMContext*, struct VFile* vf);
47static bool _seekTag(struct GBAMGMContext*, struct VFile* vf, enum GBAMGMTag tag);
48static bool _emitTag(struct GBAMGMContext*, struct VFile* vf, uint8_t tag);
49static bool _emitEnd(struct GBAMGMContext*, struct VFile* vf);
50
51static bool _parseMetadata(struct GBAMGMContext*, struct VFile* vf);
52
53static bool _markStreamNext(struct GBAMGMContext*, uint32_t newStreamId, bool recursive);
54static void _streamEndReached(struct GBAMGMContext*);
55
56static struct VFile* GBAMGMOpenSavedata(struct GBARRContext*, int flags);
57static struct VFile* GBAMGMOpenSavestate(struct GBARRContext*, int flags);
58
59void GBAMGMContextCreate(struct GBAMGMContext* mgm) {
60 memset(mgm, 0, sizeof(*mgm));
61
62 mgm->d.destroy = GBAMGMContextDestroy;
63
64 mgm->d.startPlaying = GBAMGMStartPlaying;
65 mgm->d.stopPlaying = GBAMGMStopPlaying;
66 mgm->d.startRecording = GBAMGMStartRecording;
67 mgm->d.stopRecording = GBAMGMStopRecording;
68
69 mgm->d.isPlaying = GBAMGMIsPlaying;
70 mgm->d.isRecording = GBAMGMIsRecording;
71
72 mgm->d.nextFrame = GBAMGMNextFrame;
73 mgm->d.logInput = GBAMGMLogInput;
74 mgm->d.queryInput = GBAMGMQueryInput;
75 mgm->d.queryReset = GBAMGMQueryReset;
76
77 mgm->d.stateSaved = GBAMGMStateSaved;
78 mgm->d.stateLoaded = GBAMGMStateLoaded;
79
80 mgm->d.openSavedata = GBAMGMOpenSavedata;
81 mgm->d.openSavestate = GBAMGMOpenSavestate;
82}
83
84void GBAMGMContextDestroy(struct GBARRContext* rr) {
85 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
86 if (mgm->metadataFile) {
87 mgm->metadataFile->close(mgm->metadataFile);
88 }
89}
90
91bool GBAMGMSetStream(struct GBAMGMContext* mgm, struct VDir* stream) {
92 if (mgm->movieStream && !mgm->movieStream->close(mgm->movieStream)) {
93 return false;
94 }
95
96 if (mgm->metadataFile && !mgm->metadataFile->close(mgm->metadataFile)) {
97 return false;
98 }
99
100 mgm->streamDir = stream;
101 mgm->metadataFile = mgm->streamDir->openFile(mgm->streamDir, METADATA_FILENAME, O_CREAT | O_RDWR);
102 mgm->currentInput = INVALID_INPUT;
103 if (!_parseMetadata(mgm, mgm->metadataFile)) {
104 mgm->metadataFile->close(mgm->metadataFile);
105 mgm->metadataFile = 0;
106 mgm->maxStreamId = 0;
107 }
108 mgm->streamId = 1;
109 mgm->movieStream = 0;
110 return true;
111}
112
113bool GBAMGMCreateStream(struct GBAMGMContext* mgm, enum GBARRInitFrom initFrom) {
114 if (mgm->metadataFile) {
115 mgm->metadataFile->truncate(mgm->metadataFile, 0);
116 } else {
117 mgm->metadataFile = mgm->streamDir->openFile(mgm->streamDir, METADATA_FILENAME, O_CREAT | O_TRUNC | O_RDWR);
118 }
119 _emitMagic(mgm, mgm->metadataFile);
120
121 mgm->d.initFrom = initFrom;
122 mgm->initFromOffset = mgm->metadataFile->seek(mgm->metadataFile, 0, SEEK_CUR);
123 _emitTag(mgm, mgm->metadataFile, TAG_INIT | initFrom);
124
125 mgm->streamId = 0;
126 mgm->maxStreamId = 0;
127 _emitTag(mgm, mgm->metadataFile, TAG_MAX_STREAM);
128 mgm->maxStreamIdOffset = mgm->metadataFile->seek(mgm->metadataFile, 0, SEEK_CUR);
129 mgm->metadataFile->write(mgm->metadataFile, &mgm->maxStreamId, sizeof(mgm->maxStreamId));
130
131 mgm->d.rrCount = 0;
132 _emitTag(mgm, mgm->metadataFile, TAG_RR_COUNT);
133 mgm->rrCountOffset = mgm->metadataFile->seek(mgm->metadataFile, 0, SEEK_CUR);
134 mgm->metadataFile->write(mgm->metadataFile, &mgm->d.rrCount, sizeof(mgm->d.rrCount));
135 return true;
136}
137
138bool _loadStream(struct GBAMGMContext* mgm, uint32_t streamId) {
139 if (mgm->movieStream && !mgm->movieStream->close(mgm->movieStream)) {
140 return false;
141 }
142 mgm->movieStream = 0;
143 mgm->streamId = streamId;
144 mgm->currentInput = INVALID_INPUT;
145 char buffer[14];
146 snprintf(buffer, sizeof(buffer), "%u" BINARY_EXT, streamId);
147 if (mgm->d.isRecording(&mgm->d)) {
148 int flags = O_CREAT | O_RDWR;
149 if (streamId > mgm->maxStreamId) {
150 flags |= O_TRUNC;
151 }
152 mgm->movieStream = mgm->streamDir->openFile(mgm->streamDir, buffer, flags);
153 } else if (mgm->d.isPlaying(&mgm->d)) {
154 mgm->movieStream = mgm->streamDir->openFile(mgm->streamDir, buffer, O_RDONLY);
155 mgm->peekedTag = TAG_INVALID;
156 if (!mgm->movieStream || !_verifyMagic(mgm, mgm->movieStream) || !_seekTag(mgm, mgm->movieStream, TAG_BEGIN)) {
157 mgm->d.stopPlaying(&mgm->d);
158 }
159 }
160 GBALog(0, GBA_LOG_DEBUG, "[RR] Loading segment: %u", streamId);
161 mgm->d.frames = 0;
162 mgm->d.lagFrames = 0;
163 return true;
164}
165
166bool _incrementStream(struct GBAMGMContext* mgm, bool recursive) {
167 uint32_t newStreamId = mgm->maxStreamId + 1;
168 uint32_t oldStreamId = mgm->streamId;
169 if (mgm->d.isRecording(&mgm->d) && mgm->movieStream) {
170 if (!_markStreamNext(mgm, newStreamId, recursive)) {
171 return false;
172 }
173 }
174 if (!_loadStream(mgm, newStreamId)) {
175 return false;
176 }
177 GBALog(0, GBA_LOG_DEBUG, "[RR] New segment: %u", newStreamId);
178 _emitMagic(mgm, mgm->movieStream);
179 mgm->maxStreamId = newStreamId;
180 _emitTag(mgm, mgm->movieStream, TAG_PREVIOUSLY);
181 mgm->movieStream->write(mgm->movieStream, &oldStreamId, sizeof(oldStreamId));
182 _emitTag(mgm, mgm->movieStream, TAG_BEGIN);
183
184 mgm->metadataFile->seek(mgm->metadataFile, mgm->maxStreamIdOffset, SEEK_SET);
185 mgm->metadataFile->write(mgm->metadataFile, &mgm->maxStreamId, sizeof(mgm->maxStreamId));
186 mgm->previously = oldStreamId;
187 return true;
188}
189
190bool GBAMGMStartPlaying(struct GBARRContext* rr, bool autorecord) {
191 if (rr->isRecording(rr) || rr->isPlaying(rr)) {
192 return false;
193 }
194
195 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
196 mgm->isPlaying = true;
197 if (!_loadStream(mgm, 1)) {
198 mgm->isPlaying = false;
199 return false;
200 }
201 mgm->autorecord = autorecord;
202 return true;
203}
204
205void GBAMGMStopPlaying(struct GBARRContext* rr) {
206 if (!rr->isPlaying(rr)) {
207 return;
208 }
209
210 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
211 mgm->isPlaying = false;
212 if (mgm->movieStream) {
213 mgm->movieStream->close(mgm->movieStream);
214 mgm->movieStream = 0;
215 }
216}
217
218bool GBAMGMStartRecording(struct GBARRContext* rr) {
219 if (rr->isRecording(rr) || rr->isPlaying(rr)) {
220 return false;
221 }
222
223 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
224 if (!mgm->maxStreamIdOffset) {
225 _emitTag(mgm, mgm->metadataFile, TAG_MAX_STREAM);
226 mgm->maxStreamIdOffset = mgm->metadataFile->seek(mgm->metadataFile, 0, SEEK_CUR);
227 mgm->metadataFile->write(mgm->metadataFile, &mgm->maxStreamId, sizeof(mgm->maxStreamId));
228 }
229
230 mgm->isRecording = true;
231 return _incrementStream(mgm, false);
232}
233
234void GBAMGMStopRecording(struct GBARRContext* rr) {
235 if (!rr->isRecording(rr)) {
236 return;
237 }
238
239 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
240 mgm->isRecording = false;
241 if (mgm->movieStream) {
242 _emitEnd(mgm, mgm->movieStream);
243 mgm->movieStream->close(mgm->movieStream);
244 mgm->movieStream = 0;
245 }
246}
247
248bool GBAMGMIsPlaying(const struct GBARRContext* rr) {
249 const struct GBAMGMContext* mgm = (const struct GBAMGMContext*) rr;
250 return mgm->isPlaying;
251}
252
253bool GBAMGMIsRecording(const struct GBARRContext* rr) {
254 const struct GBAMGMContext* mgm = (const struct GBAMGMContext*) rr;
255 return mgm->isRecording;
256}
257
258void GBAMGMNextFrame(struct GBARRContext* rr) {
259 if (!rr->isRecording(rr) && !rr->isPlaying(rr)) {
260 return;
261 }
262
263 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
264 if (rr->isPlaying(rr)) {
265 while (mgm->peekedTag == TAG_INPUT) {
266 _readTag(mgm, mgm->movieStream);
267 GBALog(0, GBA_LOG_WARN, "[RR] Desync detected!");
268 }
269 if (mgm->peekedTag == TAG_LAG) {
270 GBALog(0, GBA_LOG_DEBUG, "[RR] Lag frame marked in stream");
271 if (mgm->inputThisFrame) {
272 GBALog(0, GBA_LOG_WARN, "[RR] Lag frame in stream does not match movie");
273 }
274 }
275 }
276
277 ++mgm->d.frames;
278 GBALog(0, GBA_LOG_DEBUG, "[RR] Frame: %u", mgm->d.frames);
279 if (!mgm->inputThisFrame) {
280 ++mgm->d.lagFrames;
281 GBALog(0, GBA_LOG_DEBUG, "[RR] Lag frame: %u", mgm->d.lagFrames);
282 }
283
284 if (rr->isRecording(rr)) {
285 if (!mgm->inputThisFrame) {
286 _emitTag(mgm, mgm->movieStream, TAG_LAG);
287 }
288 _emitTag(mgm, mgm->movieStream, TAG_FRAME);
289 mgm->inputThisFrame = false;
290 } else {
291 if (!_seekTag(mgm, mgm->movieStream, TAG_FRAME)) {
292 _streamEndReached(mgm);
293 }
294 }
295}
296
297void GBAMGMLogInput(struct GBARRContext* rr, uint16_t keys) {
298 if (!rr->isRecording(rr)) {
299 return;
300 }
301
302 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
303 if (keys != mgm->currentInput) {
304 _emitTag(mgm, mgm->movieStream, TAG_INPUT);
305 mgm->movieStream->write(mgm->movieStream, &keys, sizeof(keys));
306 mgm->currentInput = keys;
307 }
308 GBALog(0, GBA_LOG_DEBUG, "[RR] Input log: %03X", mgm->currentInput);
309 mgm->inputThisFrame = true;
310}
311
312uint16_t GBAMGMQueryInput(struct GBARRContext* rr) {
313 if (!rr->isPlaying(rr)) {
314 return 0;
315 }
316
317 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
318 if (mgm->peekedTag == TAG_INPUT) {
319 _readTag(mgm, mgm->movieStream);
320 }
321 mgm->inputThisFrame = true;
322 if (mgm->currentInput == INVALID_INPUT) {
323 GBALog(0, GBA_LOG_WARN, "[RR] Stream did not specify input");
324 }
325 GBALog(0, GBA_LOG_DEBUG, "[RR] Input replay: %03X", mgm->currentInput);
326 return mgm->currentInput;
327}
328
329bool GBAMGMQueryReset(struct GBARRContext* rr) {
330 if (!rr->isPlaying(rr)) {
331 return 0;
332 }
333
334 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
335 return mgm->peekedTag == TAG_RESET;
336}
337
338void GBAMGMStateSaved(struct GBARRContext* rr, struct GBASerializedState* state) {
339 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
340 if (rr->isRecording(rr)) {
341 state->associatedStreamId = mgm->streamId;
342 _finishSegment(mgm);
343 }
344}
345
346void GBAMGMStateLoaded(struct GBARRContext* rr, const struct GBASerializedState* state) {
347 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
348 if (rr->isRecording(rr)) {
349 if (state->associatedStreamId != mgm->streamId) {
350 _loadStream(mgm, state->associatedStreamId);
351 _incrementStream(mgm, true);
352 } else {
353 _finishSegment(mgm);
354 }
355 _markRerecord(mgm);
356 } else if (rr->isPlaying(rr)) {
357 _loadStream(mgm, state->associatedStreamId);
358 _skipSegment(mgm);
359 }
360}
361
362bool _finishSegment(struct GBAMGMContext* mgm) {
363 if (mgm->movieStream) {
364 if (!_emitEnd(mgm, mgm->movieStream)) {
365 return false;
366 }
367 }
368 return _incrementStream(mgm, false);
369}
370
371bool _skipSegment(struct GBAMGMContext* mgm) {
372 mgm->nextTime = 0;
373 while (_readTag(mgm, mgm->movieStream) != TAG_EOF);
374 if (!mgm->nextTime || !_loadStream(mgm, mgm->nextTime)) {
375 _streamEndReached(mgm);
376 return false;
377 }
378 return true;
379}
380
381bool _markRerecord(struct GBAMGMContext* mgm) {
382 ++mgm->d.rrCount;
383 mgm->metadataFile->seek(mgm->metadataFile, mgm->rrCountOffset, SEEK_SET);
384 mgm->metadataFile->write(mgm->metadataFile, &mgm->d.rrCount, sizeof(mgm->d.rrCount));
385 return true;
386}
387
388bool _emitMagic(struct GBAMGMContext* mgm, struct VFile* vf) {
389 UNUSED(mgm);
390 return vf->write(vf, BINARY_MAGIC, 4) == 4;
391}
392
393bool _verifyMagic(struct GBAMGMContext* mgm, struct VFile* vf) {
394 UNUSED(mgm);
395 char buffer[4];
396 if (vf->read(vf, buffer, sizeof(buffer)) != sizeof(buffer)) {
397 return false;
398 }
399 if (memcmp(buffer, BINARY_MAGIC, sizeof(buffer)) != 0) {
400 return false;
401 }
402 return true;
403}
404
405enum GBAMGMTag _readTag(struct GBAMGMContext* mgm, struct VFile* vf) {
406 if (!mgm || !vf) {
407 return TAG_EOF;
408 }
409
410 enum GBAMGMTag tag = mgm->peekedTag;
411 switch (tag) {
412 case TAG_INPUT:
413 vf->read(vf, &mgm->currentInput, sizeof(uint16_t));
414 break;
415 case TAG_PREVIOUSLY:
416 vf->read(vf, &mgm->previously, sizeof(mgm->previously));
417 break;
418 case TAG_NEXT_TIME:
419 vf->read(vf, &mgm->nextTime, sizeof(mgm->nextTime));
420 break;
421 case TAG_MAX_STREAM:
422 vf->read(vf, &mgm->maxStreamId, sizeof(mgm->maxStreamId));
423 break;
424 case TAG_FRAME_COUNT:
425 vf->read(vf, &mgm->d.frames, sizeof(mgm->d.frames));
426 break;
427 case TAG_LAG_COUNT:
428 vf->read(vf, &mgm->d.lagFrames, sizeof(mgm->d.lagFrames));
429 break;
430 case TAG_RR_COUNT:
431 vf->read(vf, &mgm->d.rrCount, sizeof(mgm->d.rrCount));
432 break;
433
434 case TAG_INIT_EX_NIHILO:
435 mgm->d.initFrom = INIT_EX_NIHILO;
436 break;
437 case TAG_INIT_FROM_SAVEGAME:
438 mgm->d.initFrom = INIT_FROM_SAVEGAME;
439 break;
440 case TAG_INIT_FROM_SAVESTATE:
441 mgm->d.initFrom = INIT_FROM_SAVESTATE;
442 break;
443 case TAG_INIT_FROM_BOTH:
444 mgm->d.initFrom = INIT_FROM_BOTH;
445 break;
446
447 // To be spec'd
448 case TAG_AUTHOR:
449 case TAG_COMMENT:
450 break;
451
452 // Empty markers
453 case TAG_FRAME:
454 case TAG_LAG:
455 case TAG_RESET:
456 case TAG_BEGIN:
457 case TAG_END:
458 case TAG_INVALID:
459 case TAG_EOF:
460 break;
461 }
462
463 uint8_t tagBuffer;
464 if (vf->read(vf, &tagBuffer, 1) != 1) {
465 mgm->peekedTag = TAG_EOF;
466 } else {
467 mgm->peekedTag = tagBuffer;
468 }
469
470 if (mgm->peekedTag == TAG_END) {
471 _skipSegment(mgm);
472 }
473 return tag;
474}
475
476bool _seekTag(struct GBAMGMContext* mgm, struct VFile* vf, enum GBAMGMTag tag) {
477 enum GBAMGMTag readTag;
478 while ((readTag = _readTag(mgm, vf)) != tag) {
479 if (readTag == TAG_EOF) {
480 return false;
481 }
482 }
483 return true;
484}
485
486bool _emitTag(struct GBAMGMContext* mgm, struct VFile* vf, uint8_t tag) {
487 UNUSED(mgm);
488 return vf->write(vf, &tag, sizeof(tag)) == sizeof(tag);
489}
490
491bool _parseMetadata(struct GBAMGMContext* mgm, struct VFile* vf) {
492 if (!_verifyMagic(mgm, vf)) {
493 return false;
494 }
495 while (_readTag(mgm, vf) != TAG_EOF) {
496 switch (mgm->peekedTag) {
497 case TAG_MAX_STREAM:
498 mgm->maxStreamIdOffset = vf->seek(vf, 0, SEEK_CUR);
499 break;
500 case TAG_INIT_EX_NIHILO:
501 case TAG_INIT_FROM_SAVEGAME:
502 case TAG_INIT_FROM_SAVESTATE:
503 case TAG_INIT_FROM_BOTH:
504 mgm->initFromOffset = vf->seek(vf, 0, SEEK_CUR);
505 break;
506 case TAG_RR_COUNT:
507 mgm->rrCountOffset = vf->seek(vf, 0, SEEK_CUR);
508 break;
509 default:
510 break;
511 }
512 }
513 return true;
514}
515
516bool _emitEnd(struct GBAMGMContext* mgm, struct VFile* vf) {
517 // TODO: Error check
518 _emitTag(mgm, vf, TAG_END);
519 _emitTag(mgm, vf, TAG_FRAME_COUNT);
520 vf->write(vf, &mgm->d.frames, sizeof(mgm->d.frames));
521 _emitTag(mgm, vf, TAG_LAG_COUNT);
522 vf->write(vf, &mgm->d.lagFrames, sizeof(mgm->d.lagFrames));
523 _emitTag(mgm, vf, TAG_NEXT_TIME);
524
525 uint32_t newStreamId = 0;
526 vf->write(vf, &newStreamId, sizeof(newStreamId));
527 return true;
528}
529
530bool _markStreamNext(struct GBAMGMContext* mgm, uint32_t newStreamId, bool recursive) {
531 if (mgm->movieStream->seek(mgm->movieStream, -sizeof(newStreamId) - 1, SEEK_END) < 0) {
532 return false;
533 }
534
535 uint8_t tagBuffer;
536 if (mgm->movieStream->read(mgm->movieStream, &tagBuffer, 1) != 1) {
537 return false;
538 }
539 if (tagBuffer != TAG_NEXT_TIME) {
540 return false;
541 }
542 if (mgm->movieStream->write(mgm->movieStream, &newStreamId, sizeof(newStreamId)) != sizeof(newStreamId)) {
543 return false;
544 }
545 if (recursive) {
546 if (mgm->movieStream->seek(mgm->movieStream, 0, SEEK_SET) < 0) {
547 return false;
548 }
549 if (!_verifyMagic(mgm, mgm->movieStream)) {
550 return false;
551 }
552 _readTag(mgm, mgm->movieStream);
553 if (_readTag(mgm, mgm->movieStream) != TAG_PREVIOUSLY) {
554 return false;
555 }
556 if (mgm->previously == 0) {
557 return true;
558 }
559 uint32_t currentStreamId = mgm->streamId;
560 if (!_loadStream(mgm, mgm->previously)) {
561 return false;
562 }
563 return _markStreamNext(mgm, currentStreamId, mgm->previously);
564 }
565 return true;
566}
567
568void _streamEndReached(struct GBAMGMContext* mgm) {
569 if (!mgm->d.isPlaying(&mgm->d)) {
570 return;
571 }
572
573 uint32_t endStreamId = mgm->streamId;
574 mgm->d.stopPlaying(&mgm->d);
575 if (mgm->autorecord) {
576 mgm->isRecording = true;
577 _loadStream(mgm, endStreamId);
578 _incrementStream(mgm, false);
579 }
580}
581
582struct VFile* GBAMGMOpenSavedata(struct GBARRContext* rr, int flags) {
583 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
584 return mgm->streamDir->openFile(mgm->streamDir, "movie.sav", flags);
585}
586
587struct VFile* GBAMGMOpenSavestate(struct GBARRContext* rr, int flags) {
588 struct GBAMGMContext* mgm = (struct GBAMGMContext*) rr;
589 return mgm->streamDir->openFile(mgm->streamDir, "movie.ssm", flags);
590}