using System.Collections.Generic; using System.IO; using BizHawk.Common; using BizHawk.Emulation.Common; namespace BizHawk.Client.Common { public enum MovieMode { /// /// There is no movie loaded /// Inactive, /// /// The movie is in playback mode /// Play, /// /// The movie is currently recording /// Record, /// /// The movie has played past the end, but is still loaded in memory /// Finished } // TODO: message callback / event handler // TODO: consider other event handlers, switching modes? public interface IMovie { #region Status /// /// Gets the current movie mode /// MovieMode Mode { get; } bool IsCountingRerecords { get; set; } bool Changes { get; } #endregion #region Properties string Name { get; } /// /// Gets the total number of frames that count towards the completion time of the movie /// Possibly (but unlikely different from InputLogLength (could be infinity, or maybe an implementation automatically discounts empty frames at the end of a movie, etc) /// double FrameCount { get; } /// /// Gets the actual length of the input log, should only be used by code that iterates or needs a real length /// int InputLogLength { get; } /// /// Gets the actual length of time a movie lasts for. For subframe cores, this will be different then the above two options /// ulong TimeLength { get; } /// /// Gets the file extension for the current implementation /// string PreferredExtension { get; } /// /// Gets or sets the Sync Settings from the Core /// string SyncSettingsJson { get; set; } SubtitleList Subtitles { get; } IList Comments { get; } // savestate anchor. string TextSavestate { get; set; } byte[] BinarySavestate { get; set; } int[] SavestateFramebuffer { get; set; } // saveram anchor byte[] SaveRam { get; set; } ulong Rerecords { get; set; } bool StartsFromSavestate { get; set; } bool StartsFromSaveRam { get; set; } string GameName { get; set; } string SystemID { get; set; } string Hash { get; set; } string Author { get; set; } string Core { get; set; } string EmulatorVersion { get; set; } string FirmwareHash { get; set; } string BoardName { get; set; } /// /// Loads from the HawkFile the minimal amount of information needed to determine Header info and Movie length /// This method is intended to be more performant than a full load /// bool PreLoadHeaderAndLength(HawkFile hawkFile); /// /// Gets the header key value pairs stored in the movie file /// IDictionary HeaderEntries { get; } /// /// Forces the creation of a backup file of the current movie state /// void SaveBackup(); /// /// Creates a log generator using the given input source /// ILogEntryGenerator LogGeneratorInstance(IController source); #endregion #region File Handling API // Filename of the movie, settable by the client string Filename { get; set; } /// /// Tells the movie to load the contents of Filename /// /// Return whether or not the file was successfully loaded bool Load(bool preload); /// /// Instructs the movie to save the current contents to Filename /// void Save(); /// /// Writes the input log directly to the stream, bypassing the need to load it all into ram as a string /// void WriteInputLog(TextWriter writer); /// /// Gets one frame from the input log. /// /// The frame to get. string GetInputLogEntry(int frame); /// /// Compares the input log inside reader with the movie's current input to see if the reader's input belongs to the same timeline, /// in other words, if reader's input is completely contained in the movie's input, then it is considered in the same timeline /// /// The reader containing the contents of the input log /// Returns an error message, if any /// Returns whether or not the input log in reader is in the same timeline as the movie bool CheckTimeLines(TextReader reader, out string errorMessage); /// /// Takes reader and extracts the input log, then replaces the movies input log with it /// /// The reader containing the contents of the input log /// Returns an error message, if any bool ExtractInputLog(TextReader reader, out string errorMessage); #endregion #region Mode Handling API /// /// Tells the movie to start recording from the beginning. /// void StartNewRecording(); /// /// Tells the movie to start playback from the beginning /// void StartNewPlayback(); /// /// Sets the movie to inactive (note that it will still be in memory) /// The saveChanges flag will tell the movie to save its contents to disk /// /// if true, will save to disk /// Whether or not the movie was saved bool Stop(bool saveChanges = true); /// /// Switches to record mode /// void SwitchToRecord(); /// /// Switches to playback mode /// void SwitchToPlay(); /// /// Tells the movie to go into "Finished" mode, where the user resumes control of input but the movie is still loaded in memory /// void FinishedMode(); #endregion #region Editing API /// /// Replaces the given frame's input with an empty frame /// void ClearFrame(int frame); /// /// Adds the given input to the movie /// Note: this edits the input log without the normal movie recording logic applied /// void AppendFrame(IController source); /// /// Replaces the input at the given frame with the given input /// Note: this edits the input log without the normal movie recording logic applied /// void PokeFrame(int frame, IController source); /// /// Records the given input into the given frame, /// This is subject to normal movie recording logic /// void RecordFrame(int frame, IController source); /// /// Instructs the movie to remove all input from its input log after frame, /// After truncating, frame will be the last frame of input in the movie's input log /// /// The frame at which to truncate void Truncate(int frame); /// /// Gets a single frame of input via a controller state /// /// The frame of input to be retrieved /// A controller state representing the specified frame of input, if frame is out of range, will return null IMovieController GetInputState(int frame); #endregion } public static class MovieExtensions { public static bool IsActive(this IMovie movie) => movie?.Mode != MovieMode.Inactive; public static bool NotActive(this IMovie movie) => movie == null || movie.Mode == MovieMode.Inactive; public static bool IsPlaying(this IMovie movie) => movie?.Mode == MovieMode.Play || movie?.Mode == MovieMode.Finished; public static bool IsRecording(this IMovie movie) => movie?.Mode == MovieMode.Record; public static bool IsFinished(this IMovie movie) => movie.Mode == MovieMode.Finished; public static bool IsPlayingOrRecording(this IMovie movie) => movie?.Mode == MovieMode.Play || movie?.Mode == MovieMode.Record; /// /// If the given movie contains a savestate it will be loaded if /// the given core has savestates, and a framebuffer populated /// if it is contained in the state and the given core supports it /// public static void ProcessSavestate(this IMovie movie, IEmulator emulator) { if (emulator.HasSavestates() && movie.StartsFromSavestate) { if (movie.TextSavestate != null) { emulator.AsStatable().LoadStateText(movie.TextSavestate); } else { emulator.AsStatable().LoadStateBinary(movie.BinarySavestate); } if (movie.SavestateFramebuffer != null && emulator.HasVideoProvider()) { emulator.AsVideoProvider().PopulateFromBuffer(movie.SavestateFramebuffer); } emulator.ResetCounters(); } } /// /// Sets the given save ram if the movie contains save ram /// and the core supports save ram /// public static void ProcessSram(this IMovie movie, IEmulator emulator) { if (movie.StartsFromSaveRam && emulator.HasSaveRam()) { emulator.AsSaveRam().StoreSaveRam(movie.SaveRam); } } public static bool BoolIsPressed(this IMovie movie, int frame, string buttonName) => movie.GetInputState(frame).IsPressed(buttonName); public static float GetFloatState(this IMovie movie, int frame, string buttonName) => movie.GetInputState(frame).AxisValue(buttonName); } }