2013-12-01 20:55:52 +00:00
using System ;
using System.Collections.Generic ;
2014-11-30 16:42:58 +00:00
using System.ComponentModel ;
using System.Globalization ;
2013-12-01 20:55:52 +00:00
using System.IO ;
using System.Linq ;
2013-12-10 01:45:45 +00:00
using System.Text ;
2014-06-11 21:14:13 +00:00
using BizHawk.Common ;
2013-12-07 16:31:04 +00:00
using BizHawk.Emulation.Common ;
2014-11-30 16:42:58 +00:00
using BizHawk.Emulation.Common.IEmulatorExtensions ;
2013-12-07 16:31:04 +00:00
2013-12-01 20:55:52 +00:00
namespace BizHawk.Client.Common
{
2014-08-23 18:02:02 +00:00
public sealed partial class TasMovie : Bk2Movie , INotifyPropertyChanged
2013-12-01 20:55:52 +00:00
{
2014-11-01 12:50:36 +00:00
public const string DefaultProjectName = "default" ;
2014-08-29 02:40:45 +00:00
private readonly Bk2MnemonicConstants Mnemonics = new Bk2MnemonicConstants ( ) ;
2014-07-17 18:21:12 +00:00
private readonly TasStateManager StateManager ;
2014-11-02 00:22:04 +00:00
private readonly TasLagLog LagLog = new TasLagLog ( ) ;
2014-11-01 12:50:36 +00:00
private readonly Dictionary < int , IController > InputStateCache = new Dictionary < int , IController > ( ) ;
2014-11-01 13:37:18 +00:00
private readonly List < string > VerificationLog = new List < string > ( ) ; // For movies that do not begin with power-on, this is the input required to get into the initial state
2014-10-18 15:50:12 +00:00
2015-03-01 05:47:32 +00:00
private BackgroundWorker _progressReportWorker = null ;
2015-01-03 02:29:55 +00:00
2015-03-01 05:47:32 +00:00
public TasMovie ( string path , bool startsFromSavestate = false , BackgroundWorker progressReportWorker = null )
: base ( path )
2014-08-24 15:07:27 +00:00
{
// TODO: how to call the default constructor AND the base(path) constructor? And is base(path) calling base() ?
2015-03-01 05:47:32 +00:00
_progressReportWorker = progressReportWorker ;
2014-11-30 16:42:58 +00:00
if ( ! Global . Emulator . HasSavestates ( ) )
{
throw new InvalidOperationException ( "Cannot create a TasMovie against a core that does not implement IStatable" ) ;
}
2015-03-01 05:47:32 +00:00
ChangeLog = new TasMovieChangeLog ( this ) ;
2014-12-21 23:53:40 +00:00
StateManager = new TasStateManager ( this ) ;
2014-08-24 15:07:27 +00:00
Header [ HeaderKeys . MOVIEVERSION ] = "BizHawk v2.0 Tasproj v1.0" ;
Markers = new TasMovieMarkerList ( this ) ;
Markers . CollectionChanged + = Markers_CollectionChanged ;
2014-10-25 15:47:15 +00:00
Markers . Add ( 0 , startsFromSavestate ? "Savestate" : "Power on" ) ;
2014-08-24 15:07:27 +00:00
}
2013-12-01 22:29:38 +00:00
2015-03-01 05:47:32 +00:00
public TasMovie ( bool startsFromSavestate = false , BackgroundWorker progressReportWorker = null )
2014-06-14 21:09:14 +00:00
: base ( )
2015-03-01 05:47:32 +00:00
{
_progressReportWorker = progressReportWorker ;
2014-11-30 16:42:58 +00:00
if ( ! Global . Emulator . HasSavestates ( ) )
{
throw new InvalidOperationException ( "Cannot create a TasMovie against a core that does not implement IStatable" ) ;
}
2015-03-01 05:47:32 +00:00
ChangeLog = new TasMovieChangeLog ( this ) ;
2014-12-21 23:53:40 +00:00
StateManager = new TasStateManager ( this ) ;
2014-07-13 14:26:40 +00:00
Header [ HeaderKeys . MOVIEVERSION ] = "BizHawk v2.0 Tasproj v1.0" ;
2014-07-17 18:38:30 +00:00
Markers = new TasMovieMarkerList ( this ) ;
2014-08-24 14:41:56 +00:00
Markers . CollectionChanged + = Markers_CollectionChanged ;
2014-10-25 15:47:15 +00:00
Markers . Add ( 0 , startsFromSavestate ? "Savestate" : "Power on" ) ;
2014-06-11 21:20:23 +00:00
}
2014-11-01 12:50:36 +00:00
public TasMovieMarkerList Markers { get ; set ; }
public bool UseInputCache { get ; set ; }
2014-06-14 21:09:14 +00:00
public override string PreferredExtension
2014-06-11 21:20:23 +00:00
{
2014-07-07 18:00:25 +00:00
get { return Extension ; }
2014-06-11 21:20:23 +00:00
}
2014-10-14 13:31:14 +00:00
public TasStateManager TasStateManager
{
get { return StateManager ; }
}
public new const string Extension = "tasproj" ;
public TasMovieRecord this [ int index ]
{
get
{
return new TasMovieRecord
{
State = StateManager [ index ] ,
LogEntry = GetInputLogEntry ( index ) ,
2015-02-24 21:23:16 +00:00
Lagged = LagLog [ index + 1 ] ,
WasLagged = LagLog . History ( index + 1 )
2014-10-14 13:31:14 +00:00
} ;
}
}
2015-03-01 05:47:32 +00:00
#region Events and Handlers
2014-08-23 18:02:02 +00:00
public event PropertyChangedEventHandler PropertyChanged ;
private bool _changes ;
public override bool Changes
{
get { return _changes ; }
protected set
{
if ( _changes ! = value )
{
_changes = value ;
OnPropertyChanged ( "Changes" ) ;
}
}
}
2014-10-14 13:31:14 +00:00
// This event is Raised ony when Changes is TOGGLED.
2014-08-23 18:02:02 +00:00
private void OnPropertyChanged ( string propertyName )
{
if ( PropertyChanged ! = null )
{
2014-10-14 13:31:14 +00:00
// Raising the event when FirstName or LastName property value changed
2014-08-23 18:02:02 +00:00
PropertyChanged . Invoke ( this , new PropertyChangedEventArgs ( propertyName ) ) ;
}
}
2014-08-24 14:41:56 +00:00
void Markers_CollectionChanged ( object sender , System . Collections . Specialized . NotifyCollectionChangedEventArgs e )
{
Changes = true ;
}
2013-12-01 20:55:52 +00:00
2014-08-24 14:41:56 +00:00
#endregion
2014-07-11 18:06:18 +00:00
public void ClearChanges ( )
{
Changes = false ;
}
2014-10-20 19:04:59 +00:00
public void FlagChanges ( )
2014-10-19 14:46:01 +00:00
{
Changes = true ;
}
2014-07-08 13:33:01 +00:00
public override void StartNewRecording ( )
{
2014-11-01 12:50:36 +00:00
ClearTasprojExtras ( ) ;
2014-07-15 23:43:17 +00:00
Markers . Add ( 0 , StartsFromSavestate ? "Savestate" : "Power on" ) ;
2015-03-01 05:47:32 +00:00
ChangeLog = new TasMovieChangeLog ( this ) ;
2014-11-01 12:50:36 +00:00
base . StartNewRecording ( ) ;
2014-07-08 13:33:01 +00:00
}
2014-07-09 16:35:39 +00:00
2014-08-23 18:02:02 +00:00
public override void SwitchToPlay ( )
{
_mode = Moviemode . Play ;
}
2014-07-11 19:58:24 +00:00
/// <summary>
/// Removes lag log and greenzone after this frame
/// </summary>
2014-08-23 02:06:56 +00:00
/// <param name="frame">The last frame that can be valid.</param>
2014-07-11 19:58:24 +00:00
private void InvalidateAfter ( int frame )
{
2014-11-02 00:22:04 +00:00
LagLog . RemoveFrom ( frame ) ;
2014-07-11 19:58:24 +00:00
StateManager . Invalidate ( frame + 1 ) ;
2014-08-22 17:04:31 +00:00
Changes = true ; // TODO check if this actually removed anything before flagging changes
2014-07-11 19:58:24 +00:00
}
2014-07-10 02:45:56 +00:00
/// <summary>
/// Returns the mnemonic value for boolean buttons, and actual value for floats,
2014-08-22 17:04:31 +00:00
/// for a given frame and button.
2014-07-10 02:45:56 +00:00
/// </summary>
public string DisplayValue ( int frame , string buttonName )
{
2014-08-29 02:58:52 +00:00
if ( UseInputCache & & InputStateCache . ContainsKey ( frame ) )
{
return CreateDisplayValueForButton ( InputStateCache [ frame ] , buttonName ) ;
}
2014-07-10 02:45:56 +00:00
var adapter = GetInputState ( frame ) ;
2014-08-29 02:58:52 +00:00
if ( UseInputCache )
{
InputStateCache . Add ( frame , adapter ) ;
}
2014-07-16 23:04:56 +00:00
return CreateDisplayValueForButton ( adapter , buttonName ) ;
}
2014-08-29 02:58:52 +00:00
public void FlushInputCache ( )
{
InputStateCache . Clear ( ) ;
}
2014-08-29 01:59:08 +00:00
public string CreateDisplayValueForButton ( IController adapter , string buttonName )
2014-07-16 23:04:56 +00:00
{
2014-07-10 02:45:56 +00:00
if ( adapter . Type . BoolButtons . Contains ( buttonName ) )
{
return adapter . IsPressed ( buttonName ) ?
2014-08-29 02:40:45 +00:00
Mnemonics [ buttonName ] . ToString ( ) :
2014-07-10 02:45:56 +00:00
string . Empty ;
}
if ( adapter . Type . FloatControls . Contains ( buttonName ) )
{
2014-07-11 16:26:19 +00:00
return adapter . GetFloat ( buttonName ) . ToString ( ) ;
2014-07-10 02:45:56 +00:00
}
return "!" ;
}
2014-07-10 20:40:50 +00:00
public bool BoolIsPressed ( int frame , string buttonName )
{
2014-11-01 12:50:36 +00:00
return ( ( Bk2ControllerAdapter ) GetInputState ( frame ) )
. IsPressed ( buttonName ) ;
2014-07-10 20:40:50 +00:00
}
2014-07-10 20:48:43 +00:00
2014-07-11 16:26:19 +00:00
public float GetFloatValue ( int frame , string buttonName )
{
2014-11-01 12:50:36 +00:00
return ( ( Bk2ControllerAdapter ) GetInputState ( frame ) )
. GetFloat ( buttonName ) ;
2014-07-11 16:26:19 +00:00
}
2014-07-13 22:17:31 +00:00
// TODO: try not to need this, or at least use GetInputState and then a log entry generator
public string GetInputLogEntry ( int frame )
2014-07-10 20:48:43 +00:00
{
2014-07-13 22:17:31 +00:00
if ( frame < FrameCount & & frame > = 0 )
{
int getframe ;
if ( LoopOffset . HasValue )
{
if ( frame < _log . Count )
{
getframe = frame ;
}
else
{
getframe = ( ( frame - LoopOffset . Value ) % ( _log . Count - LoopOffset . Value ) ) + LoopOffset . Value ;
}
}
else
{
getframe = frame ;
}
return _log [ getframe ] ;
}
return string . Empty ;
2014-07-10 20:48:43 +00:00
}
2014-07-11 15:43:47 +00:00
2014-10-02 22:58:36 +00:00
public void ClearGreenzone ( )
{
2014-10-02 23:50:50 +00:00
if ( StateManager . Any ( ) )
2014-10-02 22:58:36 +00:00
{
2014-10-02 23:50:50 +00:00
StateManager . ClearGreenzone ( ) ;
2014-10-02 22:58:36 +00:00
Changes = true ;
}
}
2014-10-16 23:05:59 +00:00
public override IController GetInputState ( int frame )
{
return base . GetInputState ( frame ) ;
}
2014-10-30 23:29:21 +00:00
2015-02-24 21:56:01 +00:00
public void GreenzoneCurrentFrame ( )
{
LagLog [ Global . Emulator . Frame ] = Global . Emulator . AsInputPollable ( ) . IsLagFrame ;
if ( ! StateManager . HasState ( Global . Emulator . Frame ) )
{
StateManager . Capture ( ) ;
}
}
2014-10-30 23:29:21 +00:00
public void ClearLagLog ( )
{
LagLog . Clear ( ) ;
}
public void DeleteLogBefore ( int frame )
{
if ( frame < _log . Count )
{
_log . RemoveRange ( 0 , frame ) ;
}
}
2014-11-01 14:01:21 +00:00
public void CopyLog ( IEnumerable < string > log )
2014-10-30 23:29:21 +00:00
{
_log . Clear ( ) ;
2015-03-01 05:47:32 +00:00
foreach ( var entry in log )
2014-10-30 23:29:21 +00:00
{
_log . Add ( entry ) ;
}
}
2014-11-01 12:50:36 +00:00
2014-11-01 14:01:21 +00:00
public void CopyVerificationLog ( IEnumerable < string > log )
{
VerificationLog . Clear ( ) ;
foreach ( var entry in log )
{
VerificationLog . Add ( entry ) ;
}
}
2014-11-01 12:50:36 +00:00
public List < string > GetLogEntries ( )
{
return _log ;
}
2014-11-15 21:49:58 +00:00
private int? TimelineBranchFrame = null ;
// TODO: this is 99% copy pasting of bad code
public override bool ExtractInputLog ( TextReader reader , out string errorMessage )
{
errorMessage = string . Empty ;
int? stateFrame = null ;
var newLog = new List < string > ( ) ;
// We are in record mode so replace the movie log with the one from the savestate
if ( ! Global . MovieSession . MultiTrack . IsActive )
{
TimelineBranchFrame = null ;
if ( Global . Config . EnableBackupMovies & & MakeBackup & & _log . Any ( ) )
{
SaveBackup ( ) ;
MakeBackup = false ;
}
int counter = 0 ;
while ( true )
{
var line = reader . ReadLine ( ) ;
if ( string . IsNullOrEmpty ( line ) )
{
break ;
}
else if ( line . Contains ( "Frame 0x" ) ) // NES stores frame count in hex, yay
{
var strs = line . Split ( 'x' ) ;
try
{
stateFrame = int . Parse ( strs [ 1 ] , NumberStyles . HexNumber ) ;
}
catch
{
errorMessage = "Savestate Frame number failed to parse" ;
return false ;
}
}
else if ( line . Contains ( "Frame " ) )
{
var strs = line . Split ( ' ' ) ;
try
{
stateFrame = int . Parse ( strs [ 1 ] ) ;
}
catch
{
errorMessage = "Savestate Frame number failed to parse" ;
return false ;
}
}
else if ( line . StartsWith ( "LogKey:" ) )
{
LogKey = line . Replace ( "LogKey:" , "" ) ;
}
else if ( line [ 0 ] = = '|' )
{
newLog . Add ( line ) ;
2014-11-19 15:54:00 +00:00
if ( ! TimelineBranchFrame . HasValue & & counter < _log . Count & & line ! = _log [ counter ] )
2014-11-15 21:49:58 +00:00
{
TimelineBranchFrame = counter ;
}
counter + + ;
}
}
_log . Clear ( ) ;
_log . AddRange ( newLog ) ;
}
else //Multitrack mode
{
// TODO: consider TimelineBranchFrame here, my thinking is that there's never a scenario to invalidate state/lag data during multitrack
var i = 0 ;
while ( true )
{
var line = reader . ReadLine ( ) ;
if ( line = = null )
{
break ;
}
if ( line . Contains ( "Frame 0x" ) ) // NES stores frame count in hex, yay
{
var strs = line . Split ( 'x' ) ;
try
{
stateFrame = int . Parse ( strs [ 1 ] , NumberStyles . HexNumber ) ;
}
catch
{
errorMessage = "Savestate Frame number failed to parse" ;
return false ;
}
}
else if ( line . Contains ( "Frame " ) )
{
var strs = line . Split ( ' ' ) ;
try
{
stateFrame = int . Parse ( strs [ 1 ] ) ;
}
catch
{
errorMessage = "Savestate Frame number failed to parse" ;
return false ;
}
}
else if ( line . StartsWith ( "LogKey:" ) )
{
LogKey = line . Replace ( "LogKey:" , "" ) ;
}
else if ( line . StartsWith ( "|" ) )
{
2015-03-01 05:47:32 +00:00
SetFrame ( i , line ) ;
2014-11-15 21:49:58 +00:00
i + + ;
}
}
}
if ( ! stateFrame . HasValue )
{
errorMessage = "Savestate Frame number failed to parse" ;
}
var stateFramei = stateFrame ? ? 0 ;
if ( stateFramei > 0 & & stateFramei < _log . Count )
{
if ( ! Global . Config . VBAStyleMovieLoadState )
{
Truncate ( stateFramei ) ;
}
}
else if ( stateFramei > _log . Count ) // Post movie savestate
{
if ( ! Global . Config . VBAStyleMovieLoadState )
{
Truncate ( _log . Count ) ;
}
_mode = Moviemode . Finished ;
}
if ( IsCountingRerecords )
{
Rerecords + + ;
}
if ( TimelineBranchFrame . HasValue )
{
LagLog . RemoveFrom ( TimelineBranchFrame . Value ) ;
TasStateManager . Invalidate ( TimelineBranchFrame . Value ) ;
}
return true ;
}
2013-12-01 20:55:52 +00:00
}
}