2013-11-01 18:52:26 +00:00
using System ;
using System.IO ;
2013-11-04 01:39:19 +00:00
using BizHawk.Emulation.Common ;
2014-08-02 15:32:48 +00:00
using BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES ;
using BizHawk.Emulation.Cores.Nintendo.NES ;
using BizHawk.Emulation.Cores.Nintendo.SNES9X ;
using BizHawk.Emulation.Cores.Nintendo.SNES ;
using BizHawk.Client.Common ;
2013-11-04 01:39:19 +00:00
2013-11-01 18:52:26 +00:00
namespace BizHawk.Client.Common
2013-10-25 00:59:34 +00:00
{
2014-07-14 00:35:33 +00:00
public enum MovieEndAction { Stop , Pause , Record , Finish }
2013-10-25 00:59:34 +00:00
public class MovieSession
{
2013-12-05 00:44:56 +00:00
public MovieSession ( )
{
ReadOnly = true ;
2014-06-17 01:13:20 +00:00
MovieControllerAdapter = MovieService . DefaultInstance . LogGeneratorInstance ( ) . MovieControllerAdapter ;
2014-08-17 15:04:23 +00:00
MultiTrack = new MultitrackRecorder ( ) ;
2013-12-05 00:44:56 +00:00
}
2014-08-02 15:32:48 +00:00
/// <summary>
/// When initializing a movie, it will be stored here until Rom processes have been completed, then it will be moved to the Movie property
/// If an existing movie is still active, it will remain in the Movie property while the new movie is queued
/// </summary>
public IMovie QueuedMovie { get ; set ; }
// This wrapper but the logic could change, don't make the client code understand these details
public bool MovieIsQueued
{
get { return QueuedMovie ! = null ; }
}
2014-08-17 15:04:23 +00:00
public MultitrackRecorder MultiTrack { get ; private set ; }
2014-06-19 01:47:22 +00:00
public IMovieController MovieControllerAdapter { get ; set ; }
2013-12-05 00:44:56 +00:00
public IMovie Movie { get ; set ; }
public bool ReadOnly { get ; set ; }
public Action < string > MessageCallback { get ; set ; }
public Func < string , string , bool > AskYesNoCallback { get ; set ; }
2013-12-03 18:08:45 +00:00
2014-07-14 00:35:33 +00:00
/// <summary>
/// Required
/// </summary>
public Action PauseCallback { get ; set ; }
/// <summary>
/// Required
/// </summary>
public Action ModeChangedCallback { get ; set ; }
2014-06-18 19:34:27 +00:00
/// <summary>
/// Simply shortens the verbosity necessary otherwise
/// </summary>
/// <returns></returns>
public ILogEntryGenerator LogGeneratorInstance ( )
{
return Movie . LogGeneratorInstance ( ) ;
}
public IMovieController MovieControllerInstance ( )
{
var adapter = Movie . LogGeneratorInstance ( ) . MovieControllerAdapter ;
adapter . Type = MovieControllerAdapter . Type ;
return adapter ;
}
2014-06-24 17:12:20 +00:00
// Convenience property that gets the controller state from the movie for the most recent frame
public IController CurrentInput
{
get
{
2014-08-16 21:45:36 +00:00
if ( Movie . IsActive & & ! Movie . IsFinished & & Global . Emulator . Frame > 0 )
2014-06-24 17:12:20 +00:00
{
2014-08-16 21:45:36 +00:00
return Movie . GetInputState ( Global . Emulator . Frame - 1 ) ;
2014-06-24 17:12:20 +00:00
}
return null ;
}
}
2014-06-29 14:42:20 +00:00
public IController PreviousFrame
{
get
{
2014-08-16 21:45:36 +00:00
if ( Movie . IsActive & & ! Movie . IsFinished & & Global . Emulator . Frame > 1 )
2014-06-29 14:42:20 +00:00
{
2014-08-16 21:45:36 +00:00
return Movie . GetInputState ( Global . Emulator . Frame - 2 ) ;
2014-06-29 14:42:20 +00:00
}
return null ;
}
}
2013-11-01 18:52:26 +00:00
private void Output ( string message )
{
if ( MessageCallback ! = null )
{
MessageCallback ( message ) ;
}
}
2013-12-05 00:44:56 +00:00
public void LatchMultitrackPlayerInput ( IController playerSource , MultitrackRewiringControllerAdapter rewiredSource )
2013-11-01 18:52:26 +00:00
{
2014-08-16 21:45:36 +00:00
if ( MultiTrack . IsActive )
2013-11-01 18:52:26 +00:00
{
2013-12-05 00:44:56 +00:00
rewiredSource . PlayerSource = 1 ;
2014-08-16 21:45:36 +00:00
rewiredSource . PlayerTargetMask = 1 < < MultiTrack . CurrentPlayer ;
if ( MultiTrack . RecordAll )
2013-12-05 00:44:56 +00:00
{
rewiredSource . PlayerTargetMask = unchecked ( ( int ) 0xFFFFFFFF ) ;
}
2013-10-25 00:59:34 +00:00
2014-08-17 16:29:39 +00:00
if ( Movie . InputLogLength > Global . Emulator . Frame )
{
var input = Movie . GetInputState ( Global . Emulator . Frame ) ;
MovieControllerAdapter . LatchFromSource ( input ) ;
}
MovieControllerAdapter . LatchPlayerFromSource ( rewiredSource , MultiTrack . CurrentPlayer ) ;
}
2013-10-25 00:59:34 +00:00
}
public void LatchInputFromPlayer ( IController source )
{
2014-06-15 14:44:26 +00:00
MovieControllerAdapter . LatchFromSource ( source ) ;
2013-10-25 00:59:34 +00:00
}
/// <summary>
2013-11-01 18:52:26 +00:00
/// Latch input from the input log, if available
2013-10-25 00:59:34 +00:00
/// </summary>
public void LatchInputFromLog ( )
{
2014-07-14 00:35:33 +00:00
if ( Global . Emulator . Frame < Movie . InputLogLength - ( Global . Config . MovieEndAction = = MovieEndAction . Pause ? 1 : 0 ) ) // Pause logic is a hack for now
2013-11-29 19:55:05 +00:00
{
2014-07-13 22:36:37 +00:00
var input = Movie . GetInputState ( Global . Emulator . Frame ) ;
2014-07-13 22:17:31 +00:00
MovieControllerAdapter . LatchFromSource ( input ) ;
2014-08-17 15:04:23 +00:00
if ( MultiTrack . IsActive )
{
2014-08-17 16:00:46 +00:00
Global . MultitrackRewiringAdapter . Source = MovieControllerAdapter ;
2014-08-17 15:04:23 +00:00
}
2013-11-29 19:55:05 +00:00
}
2014-07-13 22:36:37 +00:00
else
{
2014-07-14 00:35:33 +00:00
HandlePlaybackEnd ( ) ;
2014-07-13 22:36:37 +00:00
}
2013-11-01 18:52:26 +00:00
}
2014-07-14 00:35:33 +00:00
private void HandlePlaybackEnd ( )
{
// TODO: mainform callback to update on mode change
switch ( Global . Config . MovieEndAction )
{
case MovieEndAction . Stop :
Movie . Stop ( ) ;
break ;
case MovieEndAction . Record :
Movie . SwitchToRecord ( ) ;
break ;
case MovieEndAction . Pause :
PauseCallback ( ) ; // TODO: one frame ago
break ;
default :
case MovieEndAction . Finish :
Movie . FinishedMode ( ) ;
break ;
}
ModeChangedCallback ( ) ;
}
2014-08-02 15:32:48 +00:00
// Movie Refactor TODO: delete me, any code calling this is poorly designed
2014-07-08 15:15:35 +00:00
public bool MovieLoad ( )
2014-06-21 14:33:33 +00:00
{
MovieControllerAdapter = Movie . LogGeneratorInstance ( ) . MovieControllerAdapter ;
2014-07-08 15:15:35 +00:00
return Movie . Load ( ) ;
2014-06-21 14:33:33 +00:00
}
2013-11-23 18:18:58 +00:00
public void StopMovie ( bool saveChanges = true )
2013-11-01 18:52:26 +00:00
{
2013-12-05 00:44:56 +00:00
var message = "Movie " ;
2013-11-01 18:52:26 +00:00
if ( Movie . IsRecording )
{
message + = "recording " ;
}
else if ( Movie . IsPlaying )
{
message + = "playback " ;
}
message + = "stopped." ;
if ( Movie . IsActive )
{
2014-09-27 23:44:59 +00:00
var result = Movie . Stop ( saveChanges ) ;
if ( result )
2013-11-01 18:52:26 +00:00
{
Output ( Path . GetFileName ( Movie . Filename ) + " written to disk." ) ;
}
2013-12-05 00:44:56 +00:00
2013-11-01 18:52:26 +00:00
Output ( message ) ;
2013-12-03 18:08:45 +00:00
ReadOnly = true ;
2013-11-01 18:52:26 +00:00
}
2014-07-19 16:03:12 +00:00
2014-08-03 20:34:45 +00:00
MultiTrack . Restart ( ) ;
2014-07-19 16:03:12 +00:00
ModeChangedCallback ( ) ;
2013-11-01 18:52:26 +00:00
}
2013-12-17 21:26:15 +00:00
public void HandleMovieSaveState ( TextWriter writer )
2013-11-01 18:52:26 +00:00
{
if ( Movie . IsActive )
{
2013-11-30 02:50:54 +00:00
writer . Write ( Movie . GetInputLog ( ) ) ;
2013-11-01 18:52:26 +00:00
}
}
public void ClearFrame ( )
{
if ( Movie . IsPlaying )
{
Movie . ClearFrame ( Global . Emulator . Frame ) ;
2013-12-03 18:08:45 +00:00
Output ( "Scrubbed input at frame " + Global . Emulator . Frame ) ;
2013-11-01 18:52:26 +00:00
}
}
2013-11-03 16:53:05 +00:00
public void HandleMovieOnFrameLoop ( )
2013-11-01 18:52:26 +00:00
{
if ( ! Movie . IsActive )
{
LatchInputFromPlayer ( Global . MovieInputSourceAdapter ) ;
}
else if ( Movie . IsFinished )
{
2013-12-05 00:44:56 +00:00
if ( Global . Emulator . Frame < Movie . FrameCount ) // This scenario can happen from rewinding (suddenly we are back in the movie, so hook back up to the movie
2013-11-01 18:52:26 +00:00
{
Movie . SwitchToPlay ( ) ;
LatchInputFromLog ( ) ;
}
else
{
LatchInputFromPlayer ( Global . MovieInputSourceAdapter ) ;
}
}
else if ( Movie . IsPlaying )
{
2013-11-29 19:55:05 +00:00
LatchInputFromLog ( ) ;
2014-09-18 22:54:15 +00:00
if ( Movie . IsRecording ) // The movie end situation can cause the switch to record mode, in that case we need to capture some input for this frame
2013-11-01 18:52:26 +00:00
{
2014-09-18 22:54:15 +00:00
HandleFrameLoopForRecordMode ( ) ;
}
else
{
// Movie may go into finished mode as a result from latching
if ( ! Movie . IsFinished )
2013-11-01 18:52:26 +00:00
{
2014-09-18 22:54:15 +00:00
if ( Global . ClientControls [ "Scrub Input" ] )
2013-11-01 18:52:26 +00:00
{
LatchInputFromPlayer ( Global . MovieInputSourceAdapter ) ;
2014-09-18 22:54:15 +00:00
ClearFrame ( ) ;
2013-11-01 18:52:26 +00:00
}
2014-09-18 22:54:15 +00:00
else if ( Global . Config . MoviePlaybackPokeMode )
2013-11-01 18:52:26 +00:00
{
2014-09-18 22:54:15 +00:00
LatchInputFromPlayer ( Global . MovieInputSourceAdapter ) ;
var lg = Movie . LogGeneratorInstance ( ) ;
lg . SetSource ( Global . MovieOutputHardpoint ) ;
if ( ! lg . IsEmpty )
{
LatchInputFromPlayer ( Global . MovieInputSourceAdapter ) ;
Movie . PokeFrame ( Global . Emulator . Frame , Global . MovieOutputHardpoint ) ;
}
else
{
LatchInputFromLog ( ) ;
}
2013-11-01 18:52:26 +00:00
}
}
}
}
else if ( Movie . IsRecording )
{
2014-09-18 22:54:15 +00:00
HandleFrameLoopForRecordMode ( ) ;
}
}
2013-12-03 18:08:45 +00:00
2014-09-18 22:54:15 +00:00
private void HandleFrameLoopForRecordMode ( )
{
if ( MultiTrack . IsActive )
{
LatchMultitrackPlayerInput ( Global . MovieInputSourceAdapter , Global . MultitrackRewiringAdapter ) ;
2013-11-01 18:52:26 +00:00
}
2014-09-18 22:54:15 +00:00
else
{
LatchInputFromPlayer ( Global . MovieInputSourceAdapter ) ;
}
// the movie session makes sure that the correct input has been read and merged to its MovieControllerAdapter;
// this has been wired to Global.MovieOutputHardpoint in RewireInputChain
Movie . RecordFrame ( Global . Emulator . Frame , Global . MovieOutputHardpoint ) ;
2013-11-01 18:52:26 +00:00
}
2013-11-01 20:53:47 +00:00
public bool HandleMovieLoadState ( string path )
{
using ( var sr = new StreamReader ( path ) )
{
2013-12-03 18:08:45 +00:00
return HandleMovieLoadState ( sr ) ;
2013-11-01 20:53:47 +00:00
}
}
2014-04-18 17:41:14 +00:00
//TODO: maybe someone who understands more about what's going on here could rename these step1 and step2 into something more descriptive
public bool HandleMovieLoadState_HackyStep2 ( TextReader reader )
{
if ( ! Movie . IsActive )
{
return true ;
}
if ( ReadOnly )
{
}
else
{
string errorMsg ;
//// fixme: this is evil (it causes crashes in binary states because InflaterInputStream can't have its position set, even to zero.
//((StreamReader)reader).BaseStream.Position = 0;
//((StreamReader)reader).DiscardBufferedData();
//edit: zero 18-apr-2014 - this was solved by HackyStep1 and HackyStep2, so that the zip stream can be re-acquired instead of needing its position reset
var result = Movie . ExtractInputLog ( reader , out errorMsg ) ;
if ( ! result )
{
Output ( errorMsg ) ;
return false ;
}
}
return true ;
}
2013-12-17 21:26:15 +00:00
public bool HandleMovieLoadState ( TextReader reader )
2014-04-18 17:41:14 +00:00
{
if ( ! HandleMovieLoadState_HackyStep1 ( reader ) )
return false ;
return HandleMovieLoadState_HackyStep2 ( reader ) ;
}
public bool HandleMovieLoadState_HackyStep1 ( TextReader reader )
2013-11-01 18:52:26 +00:00
{
if ( ! Movie . IsActive )
{
return true ;
}
2013-12-05 00:44:56 +00:00
string errorMsg ;
2013-12-04 14:42:24 +00:00
if ( ReadOnly )
2013-11-01 18:52:26 +00:00
{
2013-12-05 00:20:21 +00:00
var result = Movie . CheckTimeLines ( reader , out errorMsg ) ;
2013-12-04 14:42:24 +00:00
if ( ! result )
2013-11-01 18:52:26 +00:00
{
2013-12-05 00:20:21 +00:00
Output ( errorMsg ) ;
2013-12-04 14:42:24 +00:00
return false ;
2013-11-01 18:52:26 +00:00
}
2013-12-04 14:42:24 +00:00
if ( Movie . IsRecording )
2013-11-01 18:52:26 +00:00
{
2013-12-04 14:42:24 +00:00
Movie . SwitchToPlay ( ) ;
2013-11-01 18:52:26 +00:00
}
2013-12-04 14:42:24 +00:00
else if ( Movie . IsFinished )
2013-11-01 18:52:26 +00:00
{
2013-12-05 00:20:21 +00:00
LatchInputFromPlayer ( Global . MovieInputSourceAdapter ) ;
2013-11-01 18:52:26 +00:00
}
}
2013-12-04 14:42:24 +00:00
else
2013-11-01 18:52:26 +00:00
{
2013-12-05 00:20:21 +00:00
if ( Movie . IsFinished )
2013-11-01 18:52:26 +00:00
{
2013-12-05 00:20:21 +00:00
Movie . StartNewRecording ( ) ;
2013-12-04 14:42:24 +00:00
}
2013-12-05 00:20:21 +00:00
else if ( Movie . IsPlaying )
2013-12-04 14:42:24 +00:00
{
Movie . SwitchToRecord ( ) ;
}
2013-12-05 00:20:21 +00:00
2013-11-01 18:52:26 +00:00
}
return true ;
2013-10-25 00:59:34 +00:00
}
2014-06-29 23:13:44 +00:00
public void ToggleMultitrack ( )
{
if ( Movie . IsActive )
{
if ( Global . Config . VBAStyleMovieLoadState )
{
2014-08-17 00:13:00 +00:00
Output ( "Multi-track can not be used in Full Movie Loadstates mode" ) ;
2014-06-29 23:13:44 +00:00
}
else
{
2014-08-16 21:45:36 +00:00
MultiTrack . IsActive ^ = true ;
MultiTrack . SelectNone ( ) ;
2014-08-17 00:13:00 +00:00
Output ( MultiTrack . IsActive ? "MultiTrack Enabled" : "MultiTrack Disabled" ) ;
2014-06-29 23:13:44 +00:00
}
}
else
{
2014-08-17 00:13:00 +00:00
Output ( "MultiTrack cannot be enabled while not recording." ) ;
2014-06-29 23:13:44 +00:00
}
}
2014-08-02 15:32:48 +00:00
// Movie Load Refactor TODO: a better name
/// <summary>
/// Sets the Movie property with the QueuedMovie, clears the queued movie, and starts the new movie
/// </summary>
public void RunQueuedMovie ( bool recordMode )
{
Movie = QueuedMovie ;
QueuedMovie = null ;
2014-08-03 20:34:45 +00:00
MultiTrack . Restart ( ) ;
2014-08-02 15:32:48 +00:00
2014-10-18 20:55:10 +00:00
if ( recordMode )
2014-08-02 15:32:48 +00:00
{
Movie . StartNewRecording ( ) ;
ReadOnly = false ;
}
else
{
Movie . StartNewPlayback ( ) ;
}
}
public void QueueNewMovie ( IMovie movie , bool record )
{
if ( ! record ) // The semantics of record is that we are starting a new movie, and even wiping a pre-existing movie with the same path, but non-record means we are loading an existing movie into playback mode
{
movie . Load ( ) ;
if ( movie . SystemID ! = Global . Emulator . SystemId )
{
2014-09-27 15:49:39 +00:00
throw new MoviePlatformMismatchException (
string . Format (
"Movie system Id ({0}) does not match the currently loaded platform ({1}), unable to load" ,
movie . SystemID ,
Global . Emulator . SystemId ) ) ;
2014-08-02 15:32:48 +00:00
}
}
//If a movie is already loaded, save it before starting a new movie
if ( Global . MovieSession . Movie . IsActive & & ! string . IsNullOrEmpty ( Global . MovieSession . Movie . Filename ) )
{
Global . MovieSession . Movie . Save ( ) ;
}
// Note: this populates MovieControllerAdapter's Type with the approparite controller
// Don't set it to a movie instance of the adapter or you will lose the definition!
InputManager . RewireInputChain ( ) ;
if ( ! record & & Global . Emulator . SystemId = = "NES" ) // For NES we need special logic since the movie will drive which core to load
{
var quicknesName = ( ( CoreAttributes ) Attribute . GetCustomAttribute ( typeof ( QuickNES ) , typeof ( CoreAttributes ) ) ) . CoreName ;
var neshawkName = ( ( CoreAttributes ) Attribute . GetCustomAttribute ( typeof ( NES ) , typeof ( CoreAttributes ) ) ) . CoreName ;
// If either is specified use that, else use whatever is currently set
2014-08-30 19:20:09 +00:00
if ( movie . Core = = quicknesName )
2014-08-02 15:32:48 +00:00
{
Global . Config . NES_InQuickNES = true ;
}
2014-08-30 19:20:09 +00:00
else if ( movie . Core = = neshawkName )
2014-08-02 15:32:48 +00:00
{
Global . Config . NES_InQuickNES = false ;
}
}
else if ( ! record & & Global . Emulator . SystemId = = "SNES" ) // ditto with snes9x vs bsnes
{
var snes9xName = ( ( CoreAttributes ) Attribute . GetCustomAttribute ( typeof ( Snes9x ) , typeof ( CoreAttributes ) ) ) . CoreName ;
var bsnesName = ( ( CoreAttributes ) Attribute . GetCustomAttribute ( typeof ( LibsnesCore ) , typeof ( CoreAttributes ) ) ) . CoreName ;
2014-08-30 19:20:09 +00:00
if ( movie . Core = = snes9xName )
2014-08-02 15:32:48 +00:00
{
Global . Config . SNES_InSnes9x = true ;
}
else
{
Global . Config . SNES_InSnes9x = false ;
}
}
if ( record ) // This is a hack really, we need to set the movie to its propert state so that it will be considered active later
{
movie . SwitchToRecord ( ) ;
}
else
{
movie . SwitchToPlay ( ) ;
}
QueuedMovie = movie ;
}
2013-10-25 00:59:34 +00:00
}
}