all repos — mgba @ 22245617f434049f4646916d1b2930d376503b0d

mGBA Game Boy Advance Emulator

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