BizHawk/BizHawk.Client.Common/movie/TasMovie.cs

444 lines
10 KiB
C#

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<string> ActivePlayers { get; set; }
public Dictionary<string, char> AvailableMnemonics
{
get
{
return _mg.AvailableMnemonics;
}
}
public void ToggleButton(int frame, string buttonName)
{
InvalidateGreenzone(frame);
_records[frame].SetButton(buttonName, !_records[frame].Buttons[buttonName]);
}
public void SetButton(int frame, string buttonName, bool value)
{
InvalidateGreenzone(frame);
_records[frame].SetButton(buttonName, value);
}
public bool IsPressed(int frame, string buttonName)
{
return _records[frame].Buttons[buttonName];
}
private void InputChanged(object sender, MovieRecord.InputEventArgs e)
{
Changes = true;
if (OnChanged != null)
{
OnChanged(sender, e);
}
}
/// <summary>
/// Removes the greenzone content after the given frame
/// </summary>
/// <param name="frame"></param>
private void InvalidateGreenzone(int frame)
{
for (int i = frame + 1; i < _records.Count; i++)
{
_records[i].ClearState();
}
}
#region Events
public delegate void MovieEventHandler(object sender, MovieRecord.InputEventArgs e);
public event MovieEventHandler OnChanged;
#endregion
#region 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 _mg.GenerateMnemonicString(_records[frame].Buttons);
}
else
{
return String.Empty;
}
}
else
{
_mode = Moviemode.Record;
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(_mg.GenerateMnemonicString(record.Buttons));
}
return sb.ToString();
}
public void SwitchToRecord()
{
_mode = Moviemode.Record;
}
public void SwitchToPlay()
{
_mode = Moviemode.Play;
}
public void StartNewPlayback()
{
_mode = Moviemode.Play;
Global.Emulator.ClearSaveRam();
}
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
/*
if (saveChanges)
{
if (_mode == Moviemode.Record || Changes)
{
Save();
}
}
*/
_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;
_mg.Source = source;
var record = new MovieRecord(_mg.GetBoolButtons(), true);
record.OnChanged += InputChanged;
_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;
_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("|"))
{
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
}
}