all repos — mgba @ fa884d071ecaa3e05ff20b45a67bf9500dd3d6b6

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 <mgba/internal/gba/rr/mgm.h>
  7
  8#include <mgba/internal/gba/gba.h>
  9#include <mgba/internal/gba/serialize.h>
 10#include <mgba-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	mLOG(GBA_RR, DEBUG, "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	mLOG(GBA_RR, DEBUG, "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			mLOG(GBA_RR, WARN, "Desync detected!");
268		}
269		if (mgm->peekedTag == TAG_LAG) {
270			mLOG(GBA_RR, DEBUG, "Lag frame marked in stream");
271			if (mgm->inputThisFrame) {
272				mLOG(GBA_RR, WARN, "Lag frame in stream does not match movie");
273			}
274		}
275	}
276
277	++mgm->d.frames;
278	mLOG(GBA_RR, DEBUG, "Frame: %u", mgm->d.frames);
279	if (!mgm->inputThisFrame) {
280		++mgm->d.lagFrames;
281		mLOG(GBA_RR, DEBUG, "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	mLOG(GBA_RR, DEBUG, "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		mLOG(GBA_RR, WARN, "Stream did not specify input");
324	}
325	mLOG(GBA_RR, DEBUG, "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}