using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using BizHawk.Emulation.Common; namespace BizHawk.Client.Common { public class TasMovie : IMovie { // TODO: pass source into // TODO: preloading, or benchmark and see how much of a performaance gain it really is // TODO: support loop Offset // TODO: consider the fileformat of binary and lagged data private readonly IMnemonicPorts _mg; private readonly IController _source = Global.MovieOutputHardpoint; public MovieRecord this[int index] { get { return _records[index]; } } public List ActivePlayers { get; set; } public Dictionary AvailableMnemonics { get { return _mg.AvailableMnemonics; } } public void ToggleButton(int frame, string buttonName) { InvalidateGreenzone(frame); /*Serialize todo _records[frame].SetButton(buttonName, !_records[frame].Buttons[buttonName]); */ } public void SetButton(int frame, string buttonName, bool value) { InvalidateGreenzone(frame); /*Serialize TODO _records[frame].SetButton(buttonName, value); */ } public bool IsPressed(int frame, string buttonName) { return true; //Serialize TODO - _records[frame].Buttons[buttonName]; } /// /// Removes the greenzone content after the given frame /// /// private void InvalidateGreenzone(int frame) { for (int i = frame + 1; i < _records.Count; i++) { _records[i].ClearState(); } } #region IMovie Implementation public TasMovie(string filename, bool startsFromSavestate = false) : this(startsFromSavestate) { Filename = filename; } public TasMovie(bool startsFromSavestate = false) { _mg = MnemonicGeneratorFactory.Generate(); Filename = string.Empty; Header = new MovieHeader { StartsFromSavestate = startsFromSavestate }; Header[HeaderKeys.MOVIEVERSION] = HeaderKeys.MovieVersion2; _records = new MovieRecordList(); _mode = Moviemode.Inactive; IsCountingRerecords = true; } public string Filename { get; set; } public IMovieHeader Header { get; private set; } public bool IsActive { get { return _mode != Moviemode.Inactive; } } public bool IsPlaying { get { return _mode == Moviemode.Play; } } public bool IsRecording { get { return _mode == Moviemode.Record; } } public bool IsFinished { get { return false; } //a TasMovie is never in this mode. } public bool IsCountingRerecords { get; set; } public bool Changes { get; set; } public TimeSpan Time { get { double dblseconds = GetSeconds(_records.Count); int seconds = (int)(dblseconds % 60); int days = seconds / 86400; int hours = seconds / 3600; int minutes = (seconds / 60) % 60; int milliseconds = (int)((dblseconds - seconds) * 1000); return new TimeSpan(days, hours, minutes, seconds, milliseconds); } } public double FrameCount { get { return _records.Count; } } public int InputLogLength { get { return _records.Count; } } public string GetInput(int frame) { if (frame < _records.Count) { if (frame >= 0) { if (!_records[frame].HasState) { _records[frame].CaptureSate(); } return string.Empty; //Serialize TODO _mg.GenerateMnemonicString(_records[frame].Buttons); } else { return string.Empty; } } else { _mode = Moviemode.Record; /* Serialize TODO var buttons = _mg.ParseMnemonicString(_mg.EmptyMnemonic); _records.Add(new MovieRecord(buttons, true)); */ return string.Empty; } } public string GetInputLog() { StringBuilder sb = new StringBuilder(); foreach (var record in _records) { sb.AppendLine(record.SerializedInput); } return sb.ToString(); } public void SwitchToRecord() { _mode = Moviemode.Record; } public void SwitchToPlay() { _mode = Moviemode.Play; } public void StartNewPlayback() { _mode = Moviemode.Play; Global.Emulator.ClearSaveRam(); // should this exist?? } public void Stop(bool saveChanges = true) { // adelikat: I think Tastudio should be in charge of saving, and so we should not attempt to manage any logic like that here // EmuHawk client UI assumes someone has already picked a filename ahead of time and that it is in charge of movies _mode = Moviemode.Inactive; } public void Truncate(int frame) { _records.Truncate(frame); } public void ClearFrame(int frame) { if (frame < _records.Count) { Changes = true; _records[frame].ClearInput(); } } public void AppendFrame(IController source) { Changes = true; /* Serialize TODO _mg.Source = source; var record = new MovieRecord(_mg.GetBoolButtons(), true); _records.Add(record); */ } public void RecordFrame(int frame, IController source) { if (_mode == Moviemode.Record) { Changes = true; if (Global.Config.VBAStyleMovieLoadState) { if (Global.Emulator.Frame < _records.Count) { _records.Truncate(Global.Emulator.Frame); } } if (frame < _records.Count) { PokeFrame(frame, source); } else { AppendFrame(source); } } } public void PokeFrame(int frame, IController source) { InvalidateGreenzone(frame); if (frame < _records.Count) { Changes = true; _mg.Source = source; /* Serialize TODO _records[frame].SetInput(_mg.GetBoolButtons()); */ } } public double Fps { get { var system = Header[HeaderKeys.PLATFORM]; var pal = Header.ContainsKey(HeaderKeys.PAL) && Header[HeaderKeys.PAL] == "1"; return _frameRates[system, pal]; } } public void StartNewRecording() { SwitchToRecord(); // TODO: MakeBackup logic - Tastudio logic should be to always make backups before saving! if (Changes && !String.IsNullOrWhiteSpace(Filename)) { Save(); } _records.Clear(); Header.Clear(); } public bool Load() { var file = new FileInfo(Filename); if (!file.Exists) { return false; } // there's a lot of common code here with SavestateManager. refactor? using (BinaryStateLoader bl = BinaryStateLoader.LoadAndDetect(Filename)) { if (bl == null) return false; Header.Clear(); _records.Clear(); bl.GetLump(BinaryStateLump.Movieheader, true, delegate(TextReader tr) { string line; while ((line = tr.ReadLine()) != null) if (!Header.ParseLineFromFile(line)) Header.Comments.Add(line); }); bl.GetLump(BinaryStateLump.Input, true, delegate(TextReader tr) { string line = string.Empty; while (true) { line = tr.ReadLine(); if (line == null) { break; } else if (line.StartsWith("|")) { /* Serialize TODO var parsedButtons = _mg.ParseMnemonicString(line); _records.Add(new MovieRecord(parsedButtons, captureState: false)); */ } } }); if (Header.StartsFromSavestate) { // should we raise some sort of error if there's a savestate in the archive but Header.StartsFromSavestate is false? bl.GetCoreState( delegate(Stream s) { BinaryReader br = new BinaryReader(s); Global.Emulator.LoadStateBinary(br); }, delegate(Stream s) { StreamReader sr = new StreamReader(s); Global.Emulator.LoadStateText(sr); }); } bl.GetLump(BinaryStateLump.Framebuffer, false, delegate(BinaryReader br) { int i; var buff = Global.Emulator.VideoProvider.GetVideoBuffer(); try { for (i = 0; i < buff.Length; i++) { int j = br.ReadInt32(); buff[i] = j; } } catch (EndOfStreamException) { } }); } _mode = Moviemode.Play; return true; } public void Save() { // there's a lot of common code here with SavestateManager. refactor? using (FileStream fs = new FileStream(Filename, FileMode.Create, FileAccess.Write)) using (BinaryStateSaver bs = new BinaryStateSaver(fs)) { bs.PutLump(BinaryStateLump.Movieheader, (tw) => tw.WriteLine(Header.ToString())); bs.PutLump(BinaryStateLump.Input, (tw) => tw.WriteLine(GetInputLog())); if (Header.StartsFromSavestate) { #if true bs.PutLump(BinaryStateLump.CorestateText, (tw) => Global.Emulator.SaveStateText(tw)); #else bs.PutLump(BinaryStateLump.Corestate, (bw) => Global.Emulator.SaveStateBinary(bw)); #endif } } Changes = false; } public bool CheckTimeLines(TextReader reader, out string errorMessage) { throw new NotImplementedException(); } public bool ExtractInputLog(TextReader reader, out string errorMessage) { throw new NotImplementedException(); } #endregion #region Private private enum Moviemode { Inactive, Play, Record, Finished } private readonly MovieRecordList _records; private Moviemode _mode; private readonly PlatformFrameRates _frameRates = new PlatformFrameRates(); private double GetSeconds(int frameCount) { double frames = frameCount; if (frames < 1) { return 0; } var system = Header[HeaderKeys.PLATFORM]; var pal = Header.ContainsKey(HeaderKeys.PAL) && Header[HeaderKeys.PAL] == "1"; return frames / _frameRates[system, pal]; } #endregion } }