BizHawk/BizHawk.MultiClient/movie/Movie.cs

768 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Drawing;
using System.Windows.Forms;
using System.Globalization;
namespace BizHawk.MultiClient
{
public enum MOVIEMODE { INACTIVE, PLAY, RECORD, FINISHED };
public class Movie
{
public MovieHeader Header = new MovieHeader();
public SubtitleList Subtitles = new SubtitleList();
public bool MakeBackup = true; //make backup before altering movie
public bool IsText { get; private set; }
public string Filename { get; private set; }
public MOVIEMODE Mode { get; private set; }
public int Rerecords { get; private set; }
private int Frames;
private MovieLog Log = new MovieLog();
private int lastLog;
public bool StartsFromSavestate { get; private set; }
public bool Loaded { get; private set; }
public Movie(string filename, MOVIEMODE m)
{
Mode = m;
lastLog = 0;
Rerecords = 0;
this.Filename = filename;
IsText = true;
Frames = 0;
StartsFromSavestate = false;
if (filename.Length > 0)
Loaded = true;
}
public Movie()
{
Filename = "";
Mode = MOVIEMODE.INACTIVE;
IsText = true;
Frames = 0;
StartsFromSavestate = false;
Loaded = false;
}
public string SysID()
{
return Header.GetHeaderLine(MovieHeader.PLATFORM);
}
public string GUID()
{
return Header.GetHeaderLine(MovieHeader.GUID);
}
public string GetGameName()
{
return Header.GetHeaderLine(MovieHeader.GAMENAME);
}
public int Length()
{
if (Loaded)
return Log.Length();
else
return Frames;
}
public void StopMovie()
{
if (Mode == MOVIEMODE.RECORD)
WriteMovie();
Mode = MOVIEMODE.INACTIVE;
}
public void ClearSaveRAM()
{
var file = new FileInfo(PathManager.SaveRamPath(Global.Game));
if (file.Exists) file.Delete();
}
public void StartNewRecording() { StartNewRecording(true); }
public void StartNewRecording(bool truncate)
{
ClearSaveRAM();
Mode = MOVIEMODE.RECORD;
if (Global.Config.EnableBackupMovies && MakeBackup && Log.Length() > 0)
{
WriteBackup();
MakeBackup = false;
}
if(truncate) Log.Clear();
}
public void StartPlayback()
{
ClearSaveRAM();
Mode = MOVIEMODE.PLAY;
}
public void ResumeRecording()
{
Mode = MOVIEMODE.RECORD;
}
public void CommitFrame(int frameNum, IController source)
{
//if (Global.Emulator.Frame < Log.Length())
//{
// Log.Truncate(Global.Emulator.Frame);
//}
//Note: Truncation here instead of loadstate will make VBA style loadstates
//(Where an entire movie is loaded then truncated on the next frame
//this allows users to restore a movie with any savestate from that "timeline"
MnemonicsGenerator mg = new MnemonicsGenerator();
mg.SetSource(source);
Log.SetFrameAt(frameNum, mg.GetControllersAsMnemonic());
}
public string GetInputFrame(int frame)
{
lastLog = frame;
if (frame < Log.Length())
return Log.GetFrame(frame);
else
return "";
}
public void AppendFrame(string record)
{
Log.AddFrame(record);
}
public void InsertFrame(string record, int frame)
{
Log.SetFrameAt(frame, record);
}
public void WriteMovie()
{
if (!Loaded) return;
if (Filename == "") return;
Directory.CreateDirectory(new FileInfo(Filename).Directory.FullName);
if (IsText)
WriteText(Filename);
else
WriteBinary(Filename);
}
public void WriteBackup()
{
if (!Loaded) return;
if (Filename == "") return;
Directory.CreateDirectory(new FileInfo(Filename).Directory.FullName);
string BackupName = Filename;
BackupName = BackupName.Insert(Filename.LastIndexOf("."), String.Format(".{0:yyyy-MM-dd HH.mm.ss}", DateTime.Now));
Global.RenderPanel.AddMessage("Backup movie saved to " + BackupName);
if (IsText)
WriteText(BackupName);
else
WriteBinary(BackupName);
}
private void WriteText(string file)
{
if (file.Length == 0) return; //Nothing to write
int length = Log.Length();
using (StreamWriter sw = new StreamWriter(file))
{
Header.WriteText(sw);
Subtitles.WriteText(sw);
Log.WriteText(sw);
}
}
private void WriteBinary(string file)
{
}
private bool LoadText()
{
var file = new FileInfo(Filename);
if (file.Exists == false)
{
Loaded = false;
return false;
}
else
{
Header.Clear();
Log.Clear();
}
using (StreamReader sr = file.OpenText())
{
string str = "";
while ((str = sr.ReadLine()) != null)
{
if (str == "")
{
continue;
}
if (str.Contains(MovieHeader.RERECORDS))
{
str = ParseHeader(str, MovieHeader.RERECORDS);
try
{
Rerecords = int.Parse(str);
}
catch
{
Rerecords = 0;
}
}
else if (str.Contains(MovieHeader.STARTSFROMSAVESTATE))
{
str = ParseHeader(str, MovieHeader.STARTSFROMSAVESTATE);
if (str == "1")
StartsFromSavestate = true;
}
if (Header.AddHeaderFromLine(str))
continue;
if (str.StartsWith("subtitle") || str.StartsWith("sub"))
{
Subtitles.AddSubtitle(str);
}
else if (str[0] == '|')
{
Log.AddFrame(str);
}
else
{
Header.Comments.Add(str);
}
}
}
Loaded = true;
return true;
}
/// <summary>
/// Load Header information only for displaying file information in dialogs such as play movie
/// </summary>
/// <returns></returns>
public bool PreLoadText()
{
Loaded = false;
var file = new FileInfo(Filename);
if (file.Exists == false)
return false;
else
{
Header.Clear();
Log.Clear();
}
using (StreamReader sr = file.OpenText())
{
string str = "";
int length = 0;
while ((str = sr.ReadLine()) != null)
{
length += str.Length + 1;
if (str == "")
{
continue;
}
else if (Header.AddHeaderFromLine(str))
continue;
if (str.StartsWith("subtitle") || str.StartsWith("sub"))
{
Subtitles.AddSubtitle(str);
}
else if (str[0] == '|')
{
int line = str.Length + 1;
length -= line;
int lines = (int)file.Length - length;
this.Frames = lines / line;
break;
}
else
Header.Comments.Add(str);
}
sr.Close();
}
return true;
}
private bool LoadBinary()
{
return true;
}
public bool LoadMovie()
{
var file = new FileInfo(Filename);
if (file.Exists == false)
{
Loaded = false;
return false;
}
return LoadText();
}
public void DumpLogIntoSavestateText(TextWriter writer)
{
writer.WriteLine("[Input]");
string s = MovieHeader.GUID + " " + Header.GetHeaderLine(MovieHeader.GUID);
writer.WriteLine(s);
for (int x = 0; x < Log.Length(); x++)
writer.WriteLine(Log.GetFrame(x));
writer.WriteLine("[/Input]");
Global.RenderPanel.AddMessage(Log.Length().ToString() + " frames saved"); //Debug
}
public void LoadLogFromSavestateText(string path)
{
var reader = new StreamReader(path);
int stateFrame = 0;
//We are in record mode so replace the movie log with the one from the savestate
if (!Global.MovieSession.MultiTrack.IsActive)
{
if (Global.Config.EnableBackupMovies && MakeBackup && Log.Length() > 0)
{
WriteBackup();
MakeBackup = false;
}
Log.Clear();
int i = 0; //TODO: Debug remove me
while (true)
{
string line = reader.ReadLine();
if (line.Contains(".[NES")) //TODO: Remove debug
{
MessageBox.Show("OOPS! Corrupted file stream");
}
if (line == null) break;
else if (line.Trim() == "") continue;
else if (line == "[Input]") continue;
else if (line == "[/Input]") break;
else if (line.Contains("Frame 0x")) //NES stores frame count in hex, yay
{
string[] strs = line.Split('x');
try
{
stateFrame = int.Parse(strs[1], NumberStyles.HexNumber);
}
catch { Global.RenderPanel.AddMessage("Savestate Frame failed to parse"); } //TODO: message?
}
else if (line.Contains("Frame "))
{
string[] strs = line.Split(' ');
try
{
stateFrame = int.Parse(strs[1]);
}
catch { Global.RenderPanel.AddMessage("Savestate Frame failed to parse"); } //TODO: message?
}
if (line[0] == '|')
{
Log.AddFrame(line);
i++;
}
}
//Global.RenderPanel.AddMessage(i.ToString() + " input frames loaded."); //TODO: Remove debug
}
else
{
int i = 0;
while (true)
{
string line = reader.ReadLine();
if (line == null) break;
else if (line.Trim() == "") continue;
else if (line == "[Input]") continue;
else if (line == "[/Input]") break;
else if (line.Contains("Frame 0x")) //NES stores frame count in hex, yay
{
string[] strs = line.Split(' ');
try
{
stateFrame = int.Parse(strs[1], NumberStyles.HexNumber);
}
catch { } //TODO: message?
}
else if (line.Contains("Frame "))
{
string[] strs = line.Split(' ');
try
{
stateFrame = int.Parse(strs[1]);
}
catch { } //TODO: message?
}
if (line[0] == '|')
{
Log.SetFrameAt(i, line);
i++;
}
}
//Global.RenderPanel.AddMessage(i.ToString() + " input frames loaded."); //TODO: Remove debug
}
if (stateFrame > 0 && stateFrame < Log.Length())
{
Log.Truncate(Global.Emulator.Frame);
}
IncrementRerecords();
reader.Close();
}
public void IncrementRerecords()
{
Rerecords++;
Header.UpdateRerecordCount(Rerecords);
}
public void SetRerecords(int value)
{
Rerecords = value;
Header.SetHeaderLine(MovieHeader.RERECORDS, Rerecords.ToString());
}
public void SetMovieFinished()
{
if (Mode == MOVIEMODE.PLAY)
Mode = MOVIEMODE.FINISHED;
}
public string GetTime(bool preLoad)
{
string time = "";
double seconds;
if (preLoad)
seconds = GetSeconds(Frames);
else
seconds = GetSeconds(Log.Length());
int hours = ((int)seconds) / 3600;
int minutes = (((int)seconds) / 60) % 60;
double sec = seconds % 60;
if (hours > 0)
time += MakeDigits(hours) + ":";
time += MakeDigits(minutes) + ":";
time += Math.Round((decimal)sec, 2).ToString();
return time;
}
private string MakeDigits(decimal num)
{
return MakeDigits((int)num);
}
private string MakeDigits(int num)
{
if (num < 10)
return "0" + num.ToString();
else
return num.ToString();
}
private double GetSeconds(int frameCount)
{
const double NES_PAL = 50.006977968268290849;
const double NES_NTSC = (double)60.098813897440515532;
const double PCE = (7159090.90909090 / 455 / 263); //~59.826
const double SMS_NTSC = (3579545 / 262.0 / 228.0);
const double SMS_PAL = (3546893 / 313.0 / 228.0);
const double NGP = (6144000.0 / (515 * 198));
const double VBOY = (20000000 / (259 * 384 * 4)); //~50.273
const double LYNX = 59.8;
const double WSWAN = (3072000.0 / (159 * 256));
double seconds = 0;
double frames = (double)frameCount;
if (frames < 1)
return seconds;
bool pal = false; //TODO: pal flag
switch (Header.GetHeaderLine(MovieHeader.PLATFORM))
{
case "GG":
case "SG":
case "SMS":
if (pal)
return frames / SMS_PAL;
else
return frames / SMS_NTSC;
case "FDS":
case "NES":
case "SNES":
if (pal)
return frames / NES_PAL;
else
return frames / NES_NTSC;
case "PCE":
return frames / PCE;
//One Day!
case "VBOY":
return frames / VBOY;
case "NGP":
return frames / NGP;
case "LYNX":
return frames / LYNX;
case "WSWAN":
return frames / WSWAN;
//********
case "":
default:
if (pal)
return frames / 50.0;
else
return frames / 60.0;
}
}
public bool CheckTimeLines(string path, bool OnlyGUID)
{
//This function will compare the movie data to the savestate movie data to see if they match
var reader = new StreamReader(path);
MovieLog l = new MovieLog();
string line;
string GUID;
int stateFrame = 0;
while (true)
{
line = reader.ReadLine();
if (line.Trim() == "") continue;
else if (line.Contains("GUID"))
{
GUID = ParseHeader(line, MovieHeader.GUID);
if (Header.GetHeaderLine(MovieHeader.GUID) != GUID)
{
//GUID Mismatch error
var result = MessageBox.Show(GUID + " : " + Header.GetHeaderLine(MovieHeader.GUID) + "\n" +
"The savestate GUID does not match the current movie. Proceed anyway?", "GUID Mismatch error",
MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.No)
{
reader.Close();
return false;
}
}
else if (OnlyGUID)
{
reader.Close();
return true;
}
}
else if (line.Contains("Frame 0x")) //NES stores frame count in hex, yay
{
string[] strs = line.Split('x');
try
{
stateFrame = int.Parse(strs[1], NumberStyles.HexNumber);
}
catch { Global.RenderPanel.AddMessage("Savestate Frame number failed to parse"); }
}
else if (line.Contains("Frame "))
{
string[] strs = line.Split(' ');
try
{
stateFrame = int.Parse(strs[1]);
}
catch { Global.RenderPanel.AddMessage("Savestate Frame number failed to parse"); }
}
else if (line == "[Input]") continue;
else if (line == "[/Input]") break;
else if (line[0] == '|')
l.AddFrame(line);
}
reader.BaseStream.Position = 0; //Reset position because this stream may be read again by other code
if (OnlyGUID)
{
reader.Close();
return true;
}
if (stateFrame == 0 || stateFrame > l.Length())
{
stateFrame = l.Length(); //In case the frame count failed to parse, revert to using the entire state input log
}
if (Log.Length() < stateFrame)
{
//Future event error
MessageBox.Show("The savestate is from frame " + l.Length().ToString() + " which is greater than the current movie length of " +
Log.Length().ToString() + ".\nCan not load this savestate.", "Future event Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
reader.Close();
return false;
}
for (int x = 0; x < stateFrame; x++)
{
string xs = Log.GetFrame(x);
string ys = l.GetFrame(x);
if (xs != ys)
{
//TimeLine Error
MessageBox.Show("The savestate input does not match the movie input at frame " + (x + 1).ToString() + ".",
"Timeline Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
reader.Close();
return false;
}
}
reader.Close();
return true;
}
public int CompareTo(Movie Other, string parameter)
{
int compare = 0;
if (parameter == "File")
{
compare = CompareFileName(Other);
if (compare == 0)
{
compare = CompareSysID(Other);
if (compare == 0)
{
compare = CompareGameName(Other);
if (compare == 0)
compare = CompareLength(Other);
}
}
}
else if (parameter == "SysID")
{
compare = CompareSysID(Other);
if (compare == 0)
{
compare = CompareFileName(Other);
if (compare == 0)
{
compare = CompareGameName(Other);
if (compare == 0)
compare = CompareLength(Other);
}
}
}
else if (parameter == "Game")
{
compare = CompareGameName(Other);
if (compare == 0)
{
compare = CompareFileName(Other);
if (compare == 0)
{
compare = CompareSysID(Other);
if (compare == 0)
compare = CompareLength(Other);
}
}
}
else if (parameter == "Length")
{
compare = CompareLength(Other);
if (compare == 0)
{
compare = CompareFileName(Other);
if (compare == 0)
{
compare = CompareSysID(Other);
if (compare == 0)
compare = CompareGameName(Other);
}
}
}
return compare;
}
private int CompareFileName(Movie Other)
{
string otherName = Path.GetFileName(Other.Filename);
string thisName = Path.GetFileName(this.Filename);
return thisName.CompareTo(otherName);
}
private int CompareSysID(Movie Other)
{
string otherSysID = Other.SysID();
string thisSysID = this.SysID();
if (thisSysID == null && otherSysID == null)
return 0;
else if (thisSysID == null)
return -1;
else if (otherSysID == null)
return 1;
else
return thisSysID.CompareTo(otherSysID);
}
private int CompareGameName(Movie Other)
{
string otherGameName = Other.GetGameName();
string thisGameName = this.GetGameName();
if (thisGameName == null && otherGameName == null)
return 0;
else if (thisGameName == null)
return -1;
else if (otherGameName == null)
return 1;
else
return thisGameName.CompareTo(otherGameName);
}
private int CompareLength(Movie Other)
{
int otherLength = Other.Frames;
int thisLength = this.Frames;
if (thisLength < otherLength)
return -1;
else if (thisLength > otherLength)
return 1;
else
return 0;
}
private string ParseHeader(string line, string headerName)
{
string str;
int x = line.LastIndexOf(headerName) + headerName.Length;
str = line.Substring(x + 1, line.Length - x - 1);
return str;
}
public void SetStartsFromSavestate(bool savestate)
{
StartsFromSavestate = true;
Header.AddHeaderLine(MovieHeader.STARTSFROMSAVESTATE, "1");
}
}
}