using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Windows.Forms; using BizHawk.Common; using BizHawk.Common.BufferExtensions; using BizHawk.Common.IOExtensions; using BizHawk.Client.Common; using BizHawk.Bizware.BizwareGL; using BizHawk.Emulation.Common; using BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES; using BizHawk.Emulation.Cores.Nintendo.GBA; using BizHawk.Emulation.Cores.Nintendo.NES; using BizHawk.Emulation.Cores.Nintendo.SNES; using BizHawk.Emulation.Cores.Nintendo.N64; using BizHawk.Emulation.Cores.Nintendo.GBHawkLink; using BizHawk.Client.EmuHawk.ToolExtensions; using BizHawk.Client.EmuHawk.CoreExtensions; using BizHawk.Client.ApiHawk; using BizHawk.Emulation.Common.Base_Implementations; using BizHawk.Emulation.Cores.Nintendo.SNES9X; using BizHawk.Emulation.Cores.Consoles.SNK; using BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy; namespace BizHawk.Client.EmuHawk { public partial class MainForm : Form { #region Constructors and Initialization, and Tear down private void MainForm_Load(object sender, EventArgs e) { SetWindowText(); // Hide Status bar icons and general StatusBar prep MainStatusBar.Padding = new Padding(MainStatusBar.Padding.Left, MainStatusBar.Padding.Top, MainStatusBar.Padding.Left, MainStatusBar.Padding.Bottom); // Workaround to remove extra padding on right PlayRecordStatusButton.Visible = false; AVIStatusLabel.Visible = false; SetPauseStatusBarIcon(); Tools.UpdateCheatRelatedTools(null, null); RebootStatusBarIcon.Visible = false; UpdateNotification.Visible = false; _statusBarDiskLightOnImage = Properties.Resources.LightOn; _statusBarDiskLightOffImage = Properties.Resources.LightOff; _linkCableOn = Properties.Resources.connect_16x16; _linkCableOff = Properties.Resources.noconnect_16x16; UpdateCoreStatusBarButton(); if (Config.FirstBoot) { ProfileFirstBootLabel.Visible = true; } HandleToggleLightAndLink(); SetStatusBar(); // New version notification UpdateChecker.CheckComplete += (s2, e2) => { if (IsDisposed) { return; } this.BeginInvoke(() => { UpdateNotification.Visible = UpdateChecker.IsNewVersionAvailable; }); }; UpdateChecker.BeginCheck(); // Won't actually check unless enabled by user } static MainForm() { // If this isn't here, then our assembly resolving hacks wont work due to the check for MainForm.INTERIM // its.. weird. don't ask. } private CoreComm CreateCoreComm() { return new CoreComm(ShowMessageCoreComm, NotifyCoreComm) { ReleaseGLContext = o => GLManager.ReleaseGLContext(o), RequestGLContext = (major, minor, forward) => GLManager.CreateGLContext(major, minor, forward), ActivateGLContext = gl => GLManager.Activate((GLManager.ContextRef)gl), DeactivateGLContext = () => GLManager.Deactivate() }; } public MainForm(string[] args) { void SetImages() { OpenRomMenuItem.Image = Properties.Resources.OpenFile; RecentRomSubMenu.Image = Properties.Resources.Recent; CloseRomMenuItem.Image = Properties.Resources.Close; PreviousSlotMenuItem.Image = Properties.Resources.MoveLeft; NextSlotMenuItem.Image = Properties.Resources.MoveRight; ReadonlyMenuItem.Image = Properties.Resources.ReadOnly; RecentMovieSubMenu.Image = Properties.Resources.Recent; RecordMovieMenuItem.Image = Properties.Resources.RecordHS; PlayMovieMenuItem.Image = Properties.Resources.Play; StopMovieMenuItem.Image = Properties.Resources.Stop; PlayFromBeginningMenuItem.Image = Properties.Resources.restart; ImportMoviesMenuItem.Image = Properties.Resources.Import; SaveMovieMenuItem.Image = Properties.Resources.SaveAs; SaveMovieAsMenuItem.Image = Properties.Resources.SaveAs; StopMovieWithoutSavingMenuItem.Image = Properties.Resources.Stop; RecordAVMenuItem.Image = Properties.Resources.RecordHS; ConfigAndRecordAVMenuItem.Image = Properties.Resources.AVI; StopAVIMenuItem.Image = Properties.Resources.Stop; ScreenshotMenuItem.Image = Properties.Resources.camera; PauseMenuItem.Image = Properties.Resources.Pause; RebootCoreMenuItem.Image = Properties.Resources.reboot; SwitchToFullscreenMenuItem.Image = Properties.Resources.Fullscreen; ControllersMenuItem.Image = Properties.Resources.GameController; HotkeysMenuItem.Image = Properties.Resources.HotKeys; DisplayConfigMenuItem.Image = Properties.Resources.tvIcon; SoundMenuItem.Image = Properties.Resources.AudioHS; PathsMenuItem.Image = Properties.Resources.CopyFolderHS; FirmwaresMenuItem.Image = Properties.Resources.pcb; MessagesMenuItem.Image = Properties.Resources.MessageConfig; AutofireMenuItem.Image = Properties.Resources.Lightning; RewindOptionsMenuItem.Image = Properties.Resources.Previous; ProfilesMenuItem.Image = Properties.Resources.user_blue_small; N64VideoPluginSettingsMenuItem.Image = Properties.Resources.monitor; SaveConfigMenuItem.Image = Properties.Resources.Save; LoadConfigMenuItem.Image = Properties.Resources.LoadConfig; ToolBoxMenuItem.Image = Properties.Resources.ToolBox; RamWatchMenuItem.Image = Properties.Resources.watch; RamSearchMenuItem.Image = Properties.Resources.search; LuaConsoleMenuItem.Image = Properties.Resources.Lua; TAStudioMenuItem.Image = Properties.Resources.TAStudio; HexEditorMenuItem.Image = Properties.Resources.poke; TraceLoggerMenuItem.Image = Properties.Resources.pencil; DebuggerMenuItem.Image = Properties.Resources.Bug; CodeDataLoggerMenuItem.Image = Properties.Resources.cdlogger; VirtualPadMenuItem.Image = Properties.Resources.GameController; CheatsMenuItem.Image = Properties.Resources.Freeze; GameSharkConverterMenuItem.Image = Properties.Resources.Shark; MultiDiskBundlerFileMenuItem.Image = Properties.Resources.SaveConfig; NesControllerSettingsMenuItem.Image = Properties.Resources.GameController; NESGraphicSettingsMenuItem.Image = Properties.Resources.tvIcon; NESSoundChannelsMenuItem.Image = Properties.Resources.AudioHS; PceControllerSettingsMenuItem.Image = Properties.Resources.GameController; PCEGraphicsSettingsMenuItem.Image = Properties.Resources.tvIcon; KeypadMenuItem.Image = Properties.Resources.calculator; PSXControllerSettingsMenuItem.Image = Properties.Resources.GameController; SNESControllerConfigurationMenuItem.Image = Properties.Resources.GameController; SnesGfxDebuggerMenuItem.Image = Properties.Resources.Bug; ColecoControllerSettingsMenuItem.Image = Properties.Resources.GameController; N64PluginSettingsMenuItem.Image = Properties.Resources.monitor; N64ControllerSettingsMenuItem.Image = Properties.Resources.GameController; IntVControllerSettingsMenuItem.Image = Properties.Resources.GameController; OnlineHelpMenuItem.Image = Properties.Resources.Help; ForumsMenuItem.Image = Properties.Resources.TAStudio; FeaturesMenuItem.Image = Properties.Resources.kitchensink; AboutMenuItem.Image = Properties.Resources.CorpHawkSmall; DumpStatusButton.Image = Properties.Resources.Blank; PlayRecordStatusButton.Image = Properties.Resources.Blank; PauseStatusButton.Image = Properties.Resources.Blank; RebootStatusBarIcon.Image = Properties.Resources.reboot; AVIStatusLabel.Image = Properties.Resources.Blank; LedLightStatusLabel.Image = Properties.Resources.LightOff; KeyPriorityStatusLabel.Image = Properties.Resources.Both; CoreNameStatusBarButton.Image = Properties.Resources.CorpHawkSmall; ProfileFirstBootLabel.Image = Properties.Resources.user_blue_small; LinkConnectStatusBarButton.Image = Properties.Resources.connect_16x16; OpenRomContextMenuItem.Image = Properties.Resources.OpenFile; LoadLastRomContextMenuItem.Image = Properties.Resources.Recent; StopAVContextMenuItem.Image = Properties.Resources.Stop; RecordMovieContextMenuItem.Image = Properties.Resources.RecordHS; PlayMovieContextMenuItem.Image = Properties.Resources.Play; RestartMovieContextMenuItem.Image = Properties.Resources.restart; StopMovieContextMenuItem.Image = Properties.Resources.Stop; LoadLastMovieContextMenuItem.Image = Properties.Resources.Recent; StopNoSaveContextMenuItem.Image = Properties.Resources.Stop; SaveMovieContextMenuItem.Image = Properties.Resources.SaveAs; SaveMovieAsContextMenuItem.Image = Properties.Resources.SaveAs; UndoSavestateContextMenuItem.Image = Properties.Resources.undo; toolStripMenuItem6.Image = Properties.Resources.GameController; toolStripMenuItem7.Image = Properties.Resources.HotKeys; toolStripMenuItem8.Image = Properties.Resources.tvIcon; toolStripMenuItem9.Image = Properties.Resources.AudioHS; toolStripMenuItem10.Image = Properties.Resources.CopyFolderHS; toolStripMenuItem11.Image = Properties.Resources.pcb; toolStripMenuItem12.Image = Properties.Resources.MessageConfig; toolStripMenuItem13.Image = Properties.Resources.Lightning; toolStripMenuItem14.Image = Properties.Resources.Previous; toolStripMenuItem66.Image = Properties.Resources.Save; toolStripMenuItem67.Image = Properties.Resources.LoadConfig; ScreenshotContextMenuItem.Image = Properties.Resources.camera; CloseRomContextMenuItem.Image = Properties.Resources.Close; } GlobalWin.MainForm = this; Rewinder = new Rewinder { MessageCallback = AddOnScreenMessage }; Global.ControllerInputCoalescer = new ControllerInputCoalescer(); Global.FirmwareManager = new FirmwareManager(); MovieSession = new MovieSession { Movie = MovieService.DefaultInstance, MovieControllerAdapter = MovieService.DefaultInstance.LogGeneratorInstance().MovieControllerAdapter, MessageCallback = AddOnScreenMessage, PopupCallback = ShowMessageCoreComm, AskYesNoCallback = StateErrorAskUser, PauseCallback = PauseEmulator, ModeChangedCallback = SetMainformMovieInfo }; Icon = Properties.Resources.logo; InitializeComponent(); SetImages(); Game = GameInfo.NullInstance; _throttle = new Throttle(); var comm = CreateCoreComm(); Emulator = new NullEmulator(comm); GlobalWin.Tools = new ToolManager(this, Config, Emulator); Global.CheatList = new CheatCollection(); CheatList.Changed += Tools.UpdateCheatRelatedTools; UpdateStatusSlots(); UpdateKeyPriorityIcon(); // In order to allow late construction of this database, we hook up a delegate here to dearchive the data and provide it on demand // we could background thread this later instead if we wanted to be real clever NES.BootGodDB.GetDatabaseBytes = () => { string xmlPath = Path.Combine(PathManager.GetExeDirectoryAbsolute(), "gamedb", "NesCarts.xml"); string x7zPath = Path.Combine(PathManager.GetExeDirectoryAbsolute(), "gamedb", "NesCarts.7z"); bool loadXml = File.Exists(xmlPath); using var nesCartFile = new HawkFile(loadXml ? xmlPath : x7zPath); if (!loadXml) { nesCartFile.BindFirst(); } return nesCartFile .GetStream() .ReadAllBytes(); }; try { _argParser.ParseArguments(args); } catch (ArgParserException e) { MessageBox.Show(e.Message); } Database.LoadDatabase(Path.Combine(PathManager.GetExeDirectoryAbsolute(), "gamedb", "gamedb.txt")); // TODO GL - a lot of disorganized wiring-up here // installed separately on Unix (via package manager or from https://developer.nvidia.com/cg-toolkit-download), look in $PATH CGC.CGCBinPath = OSTailoredCode.IsUnixHost ? "cgc" : Path.Combine(PathManager.GetDllDirectory(), "cgc.exe"); PresentationPanel = new PresentationPanel(this, Config, GlobalWin.GL) { GraphicsControl = { MainWindow = true } }; GlobalWin.DisplayManager = new DisplayManager(PresentationPanel); Controls.Add(PresentationPanel); Controls.SetChildIndex(PresentationPanel, 0); // TODO GL - move these event handlers somewhere less obnoxious line in the On* overrides Load += (o, e) => { AllowDrop = true; DragEnter += FormDragEnter; DragDrop += FormDragDrop; }; Closing += (o, e) => { if (Tools.AskSave()) { // zero 03-nov-2015 - close game after other steps. tools might need to unhook themselves from a core. MovieSession.Movie.Stop(); Tools.Close(); CloseGame(); // does this need to be last for any particular reason? do tool dialogs persist settings when closing? SaveConfig(); } else { e.Cancel = true; } }; ResizeBegin += (o, e) => { _inResizeLoop = true; Sound?.StopSound(); }; Resize += (o, e) => { SetWindowText(); }; ResizeEnd += (o, e) => { _inResizeLoop = false; SetWindowText(); if (PresentationPanel != null) { PresentationPanel.Resized = true; } Sound?.StartSound(); }; Input.Initialize(this); InitControls(); CoreFileProvider.SyncCoreCommInputSignals(comm); Global.ActiveController = new Controller(NullController.Instance.Definition); Global.AutoFireController = _autofireNullControls; Global.AutofireStickyXORAdapter.SetOnOffPatternFromConfig(); try { GlobalWin.Sound = new Sound(Handle); } catch { string message = "Couldn't initialize sound device! Try changing the output method in Sound config."; if (Config.SoundOutputMethod == ESoundOutputMethod.DirectSound) { message = "Couldn't initialize DirectSound! Things may go poorly for you. Try changing your sound driver to 44.1khz instead of 48khz in mmsys.cpl."; } MessageBox.Show(message, "Initialization Error", MessageBoxButtons.OK, MessageBoxIcon.Error); Config.SoundOutputMethod = ESoundOutputMethod.Dummy; GlobalWin.Sound = new Sound(Handle); } Sound.StartSound(); InputManager.RewireInputChain(); RewireSound(); // Workaround for windows, location is -32000 when minimized, if they close it during this time, that's what gets saved if (Config.MainWndx == -32000) { Config.MainWndx = 0; } if (Config.MainWndy == -32000) { Config.MainWndy = 0; } if (Config.MainWndx != -1 && Config.MainWndy != -1 && Config.SaveWindowPosition) { Location = new Point(Config.MainWndx, Config.MainWndy); } if (_argParser.cmdRom != null) { // Commandline should always override auto-load var ioa = OpenAdvancedSerializer.ParseWithLegacy(_argParser.cmdRom); LoadRom(_argParser.cmdRom, new LoadRomArgs { OpenAdvanced = ioa }); if (Game == null) { MessageBox.Show($"Failed to load {_argParser.cmdRom} specified on commandline"); } } else if (Config.RecentRoms.AutoLoad && !Config.RecentRoms.Empty) { LoadRomFromRecent(Config.RecentRoms.MostRecent); } if (_argParser.audiosync.HasValue) { Config.VideoWriterAudioSync = _argParser.audiosync.Value; } if (_argParser.cmdMovie != null) { _suppressSyncSettingsWarning = true; // We don't want to be nagged if we are attempting to automate if (Game == null) { OpenRom(); } // If user picked a game, then do the commandline logic if (!Game.IsNullInstance()) { var movie = MovieService.Get(_argParser.cmdMovie); MovieSession.ReadOnly = true; // if user is dumping and didn't supply dump length, make it as long as the loaded movie if (_argParser._autoDumpLength == 0) { _argParser._autoDumpLength = movie.InputLogLength; } // Copy pasta from drag & drop if (MovieImport.IsValidMovieExtension(Path.GetExtension(_argParser.cmdMovie))) { ProcessMovieImport(_argParser.cmdMovie, true); } else { StartNewMovie(movie, false); Config.RecentMovies.Add(_argParser.cmdMovie); } _suppressSyncSettingsWarning = false; } } else if (Config.RecentMovies.AutoLoad && !Config.RecentMovies.Empty) { if (Game.IsNullInstance()) { OpenRom(); } // If user picked a game, then do the autoload logic if (!Game.IsNullInstance()) { if (File.Exists(Config.RecentMovies.MostRecent)) { StartNewMovie(MovieService.Get(Config.RecentMovies.MostRecent), false); } else { Config.RecentMovies.HandleLoadError(Config.RecentMovies.MostRecent); } } } if (_argParser.startFullscreen || Config.StartFullscreen) { _needsFullscreenOnLoad = true; } if (!Game.IsNullInstance()) { if (_argParser.cmdLoadState != null) { LoadState(_argParser.cmdLoadState, Path.GetFileName(_argParser.cmdLoadState)); } else if (_argParser.cmdLoadSlot != null) { LoadQuickSave($"QuickSave{_argParser.cmdLoadSlot}"); } else if (Config.AutoLoadLastSaveSlot) { LoadQuickSave($"QuickSave{Config.SaveSlot}"); } } //start Lua Console if requested in the command line arguments if (_argParser.luaConsole) { Tools.Load(); } //load Lua Script if requested in the command line arguments if (_argParser.luaScript != null) { if (OSTailoredCode.IsUnixHost) Console.WriteLine($"The Lua environment can currently only be created on Windows, {_argParser.luaScript} will not be loaded."); else Tools.LuaConsole.LoadLuaFile(_argParser.luaScript); } SetStatusBar(); if (Config.StartPaused) { PauseEmulator(); } // start dumping, if appropriate if (_argParser.cmdDumpType != null && _argParser.cmdDumpName != null) { if (OSTailoredCode.IsUnixHost) Console.WriteLine("A/V dump requires Win32 API calls, ignored"); else RecordAv(_argParser.cmdDumpType, _argParser.cmdDumpName); } SetMainformMovieInfo(); SynchChrome(); PresentationPanel.Control.Paint += (o, e) => { // I would like to trigger a repaint here, but this isn't done yet }; if (!OSTailoredCode.IsUnixHost && !Config.SkipOutdatedOsCheck) { static string GetRegValue(string key) { using var proc = OSTailoredCode.ConstructSubshell("REG", $@"QUERY ""HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion"" /V {key}"); proc.Start(); return proc.StandardOutput.ReadToEnd().Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)[1].Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries)[2]; } var winVer = float.Parse(GetRegValue("CurrentVersion"), NumberFormatInfo.InvariantInfo); if (winVer < 6.3f) { // less than is just easier than equals string message = ($"Quick reminder: Windows {(winVer < 6.2f ? winVer < 6.1f ? winVer < 6.0f ? "XP" : "Vista" : "7" : "8")} is no longer supported by Microsoft. EmuHawk will continue to work, but please get a new operating system for increased security (either Windows 8.1, Windows 10, or a GNU+Linux distro)."); } else if (GetRegValue("ProductName").Contains("Windows 10")) { var win10version = int.Parse(GetRegValue("ReleaseId")); if (win10version < 1809) { string message = ($"Quick reminder: version {win10version} of Windows 10 is no longer supported by Microsoft. EmuHawk will continue to work, but please update to at least 1809 \"Redstone 5\" for increased security."); } } else { // 8.1: can't be bothered writing code for KB installed check, not that I have a Win8.1 machine to test on anyway, so it gets a free pass --yoshi } } } private readonly bool _suppressSyncSettingsWarning; public int ProgramRunLoop() { CheckMessages(); // can someone leave a note about why this is needed? // needs to be done late, after the log console snaps on top // fullscreen should snap on top even harder! if (_needsFullscreenOnLoad) { _needsFullscreenOnLoad = false; ToggleFullscreen(); } // Simply exit the program if the version is asked for if (_argParser.printVersion) { // Print the version Console.WriteLine(VersionInfo.GetEmuVersion()); // Return and leave return _exitCode; } // incantation required to get the program reliably on top of the console window // we might want it in ToggleFullscreen later, but here, it needs to happen regardless BringToFront(); Activate(); BringToFront(); InitializeFpsData(); for (; ; ) { Input.Instance.Update(); // handle events and dispatch as a hotkey action, or a hotkey button, or an input button ProcessInput(); ClientControls.LatchFromPhysical(_hotkeyCoalescer); Global.ActiveController.LatchFromPhysical(Global.ControllerInputCoalescer); Global.ActiveController.ApplyAxisConstraints( (Emulator is N64 && Config.N64UseCircularAnalogConstraint) ? "Natural Circle" : null); Global.ActiveController.OR_FromLogical(ClickyVirtualPadController); AutoFireController.LatchFromPhysical(Global.ControllerInputCoalescer); if (ClientControls["Autohold"]) { Global.StickyXORAdapter.MassToggleStickyState(Global.ActiveController.PressedButtons); Global.AutofireStickyXORAdapter.MassToggleStickyState(AutoFireController.PressedButtons); } else if (ClientControls["Autofire"]) { Global.AutofireStickyXORAdapter.MassToggleStickyState(Global.ActiveController.PressedButtons); } // autohold/autofire must not be affected by the following inputs Global.ActiveController.Overrides(Global.ButtonOverrideAdaptor); if (Tools.Has() && !SuppressLua) { Tools.LuaConsole.ResumeScripts(false); } StepRunLoop_Core(); StepRunLoop_Throttle(); Render(); CheckMessages(); if (_exitRequestPending) { _exitRequestPending = false; Close(); } if (_windowClosedAndSafeToExitProcess) { break; } if (Config.DispSpeedupFeatures != 0) { Thread.Sleep(0); } } Shutdown(); return _exitCode; } /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { // NOTE: this gets called twice sometimes. once by using() in Program.cs and once from winforms internals when the form is closed... if (DisplayManager != null) { DisplayManager.Dispose(); GlobalWin.DisplayManager = null; } if (disposing) { components?.Dispose(); } base.Dispose(disposing); } #endregion #region Pause private bool _emulatorPaused; public bool EmulatorPaused { get => _emulatorPaused; private set { if (_emulatorPaused && !value) // Unpausing { InitializeFpsData(); } _emulatorPaused = value; OnPauseChanged?.Invoke(this, new PauseChangedEventArgs(_emulatorPaused)); } } public delegate void PauseChangedEventHandler(object sender, PauseChangedEventArgs e); public event PauseChangedEventHandler OnPauseChanged; public class PauseChangedEventArgs : EventArgs { public PauseChangedEventArgs(bool paused) { Paused = paused; } public bool Paused { get; } } #endregion #region Properties public string CurrentlyOpenRom { get; private set; } // todo - delete me and use only args instead public LoadRomArgs CurrentlyOpenRomArgs { get; private set; } public bool PauseAvi { get; set; } public bool PressFrameAdvance { get; set; } public bool HoldFrameAdvance { get; set; } // necessary for tastudio > button public bool PressRewind { get; set; } // necessary for tastudio < button public bool FastForward { get; set; } /// /// Disables updates for video/audio, and enters "turbo" mode. /// Can be used to replicate Gens-rr's "latency compensation" that involves: /// /// Saving a no-framebuffer state that is stored in RAM /// Emulating forth for some frames with updates disabled /// /// Optionally hacking in-game memory /// (like camera position, to show off-screen areas) /// /// Updating the screen /// Loading the no-framebuffer state from RAM /// /// The most common use case is CamHack for Sonic games. /// Accessing this from Lua allows to keep internal code hacks to minimum. /// /// /// /// /// public bool InvisibleEmulation { get; set; } // runloop won't exec lua public bool SuppressLua { get; set; } public long MouseWheelTracker { get; private set; } private int? _pauseOnFrame; public int? PauseOnFrame // If set, upon completion of this frame, the client wil pause { get => _pauseOnFrame; set { _pauseOnFrame = value; SetPauseStatusBarIcon(); if (value == null) // TODO: make an Event handler instead, but the logic here is that after turbo seeking, tools will want to do a real update when the emulator finally pauses { bool skipScripts = !(Config.TurboSeek && !Config.RunLuaDuringTurbo && !SuppressLua); Tools.UpdateToolsBefore(skipScripts); Tools.UpdateToolsAfter(skipScripts); } } } public bool IsSeeking => PauseOnFrame.HasValue; private bool IsTurboSeeking => PauseOnFrame.HasValue && Config.TurboSeek; public bool IsTurboing => ClientControls["Turbo"] || IsTurboSeeking; #endregion #region Public Methods public void AddOnScreenMessage(string message) { GlobalWin.OSD.AddMessage(message); } public void ClearHolds() { Global.StickyXORAdapter.ClearStickies(); Global.AutofireStickyXORAdapter.ClearStickies(); if (Tools.Has()) { Tools.VirtualPad.ClearVirtualPadHolds(); } } public void FlagNeedsReboot() { RebootStatusBarIcon.Visible = true; AddOnScreenMessage("Core reboot needed for this setting"); } /// /// Controls whether the app generates input events. should be turned off for most modal dialogs /// public Input.AllowInput AllowInput(bool yieldAlt) { // the main form gets input if (ActiveForm == this) { return Input.AllowInput.All; } // even more special logic for TAStudio: // TODO - implement by event filter in TAStudio if (ActiveForm is TAStudio maybeTAStudio) { if (yieldAlt || maybeTAStudio.IsInMenuLoop) { return Input.AllowInput.None; } } // modals that need to capture input for binding purposes get input, of course if (ActiveForm is HotkeyConfig || ActiveForm is ControllerConfig || ActiveForm is TAStudio || ActiveForm is VirtualpadTool) { return Input.AllowInput.All; } // if no form is active on this process, then the background input setting applies if (ActiveForm == null && Config.AcceptBackgroundInput) { return Config.AcceptBackgroundInputControllerOnly ? Input.AllowInput.OnlyController : Input.AllowInput.All; } return Input.AllowInput.None; } // TODO: make these actual properties // This is a quick hack to reduce the dependency on Globals private IEmulator Emulator { get => Global.Emulator; set { Global.Emulator = value; _currentVideoProvider = Global.Emulator.AsVideoProviderOrDefault(); _currentSoundProvider = Global.Emulator.AsSoundProviderOrDefault(); } } private IVideoProvider _currentVideoProvider = NullVideo.Instance; private ISoundProvider _currentSoundProvider = new NullSound(44100 / 60); // Reasonable default until we have a core instance private Config Config { get => Global.Config; set => Global.Config = value; } private ToolManager Tools => GlobalWin.Tools; private DisplayManager DisplayManager => GlobalWin.DisplayManager; private IMovieSession MovieSession { get => Global. MovieSession; set => Global.MovieSession = value; } private GameInfo Game { get => Global.Game; set => Global.Game = value; } private GLManager GLManager => GlobalWin.GLManager; private Sound Sound => GlobalWin.Sound; private CheatCollection CheatList => Global.CheatList; private AutofireController AutoFireController => Global.AutoFireController; private AutoFireStickyXorAdapter AutofireStickyXORAdapter => Global.AutofireStickyXORAdapter; private ClickyVirtualPadController ClickyVirtualPadController => Global.ClickyVirtualPadController; private Rewinder Rewinder { get; } private FirmwareManager FirmwareManager => Global.FirmwareManager; private Controller ClientControls => Global.ClientControls; protected override void OnActivated(EventArgs e) { base.OnActivated(e); Input.Instance.ControlInputFocus(this, Input.InputFocus.Mouse, true); } protected override void OnDeactivate(EventArgs e) { Input.Instance.ControlInputFocus(this, Input.InputFocus.Mouse, false); base.OnDeactivate(e); } private void ProcessInput() { var conInput = (ControllerInputCoalescer)Global.ControllerInputCoalescer; for (; ; ) { // loop through all available events var ie = Input.Instance.DequeueEvent(); if (ie == null) { break; } // useful debugging: // Console.WriteLine(ie); // TODO - wonder what happens if we pop up something interactive as a response to one of these hotkeys? may need to purge further processing // look for hotkey bindings for this key var triggers = ClientControls.SearchBindings(ie.LogicalButton.ToString()); if (triggers.Count == 0) { // Maybe it is a system alt-key which hasn't been overridden if (ie.EventType == Input.InputEventType.Press) { if (ie.LogicalButton.Alt && ie.LogicalButton.Button.Length == 1) { var c = ie.LogicalButton.Button.ToLower()[0]; if ((c >= 'a' && c <= 'z') || c == ' ') { SendAltKeyChar(c); } } if (ie.LogicalButton.Alt && ie.LogicalButton.Button == "Space") { SendPlainAltKey(32); } } // ordinarily, an alt release with nothing else would move focus to the MenuBar. but that is sort of useless, and hard to implement exactly right. } // zero 09-sep-2012 - all input is eligible for controller input. not sure why the above was done. // maybe because it doesn't make sense to me to bind hotkeys and controller inputs to the same keystrokes bool handled; switch (Config.InputHotkeyOverrideOptions) { default: case 0: // Both allowed conInput.Receive(ie); handled = false; if (ie.EventType == Input.InputEventType.Press) { handled = triggers.Aggregate(handled, (current, trigger) => current | CheckHotkey(trigger)); } // hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons if (!handled) { _hotkeyCoalescer.Receive(ie); } break; case 1: // Input overrides Hotkeys conInput.Receive(ie); if (!Global.ActiveController.HasBinding(ie.LogicalButton.ToString())) { handled = false; if (ie.EventType == Input.InputEventType.Press) { handled = triggers.Aggregate(false, (current, trigger) => current | CheckHotkey(trigger)); } // hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons if (!handled) { _hotkeyCoalescer.Receive(ie); } } break; case 2: // Hotkeys override Input handled = false; if (ie.EventType == Input.InputEventType.Press) { handled = triggers.Aggregate(false, (current, trigger) => current | CheckHotkey(trigger)); } // hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons if (!handled) { _hotkeyCoalescer.Receive(ie); // Check for hotkeys that may not be handled through CheckHotkey() method, reject controller input mapped to these if (!triggers.Any(IsInternalHotkey)) { conInput.Receive(ie); } } break; } } // foreach event // also handle floats conInput.AcceptNewFloats(Input.Instance.GetFloats().Select(o => { // hackish if (o.Item1 == "WMouse X") { var p = DisplayManager.UntransformPoint(new Point((int)o.Item2, 0)); float x = p.X / (float)_currentVideoProvider.BufferWidth; return new Tuple("WMouse X", (x * 20000) - 10000); } if (o.Item1 == "WMouse Y") { var p = DisplayManager.UntransformPoint(new Point(0, (int)o.Item2)); float y = p.Y / (float)_currentVideoProvider.BufferHeight; return new Tuple("WMouse Y", (y * 20000) - 10000); } return o; })); } public void RebootCore() { if (CurrentlyOpenRomArgs == null) return; LoadRom(CurrentlyOpenRomArgs.OpenAdvanced.SimplePath, CurrentlyOpenRomArgs); } public void PauseEmulator() { EmulatorPaused = true; SetPauseStatusBarIcon(); } public void UnpauseEmulator() { EmulatorPaused = false; SetPauseStatusBarIcon(); } public void TogglePause() { EmulatorPaused ^= true; SetPauseStatusBarIcon(); // TODO: have tastudio set a pause status change callback, or take control over pause if (Tools.Has()) { Tools.UpdateValues(); } } public void TakeScreenshotToClipboard() { using (var bb = Config.ScreenshotCaptureOsd ? CaptureOSD() : MakeScreenshotImage()) { using var img = bb.ToSysdrawingBitmap(); Clipboard.SetImage(img); } AddOnScreenMessage("Screenshot (raw) saved to clipboard."); } private void TakeScreenshotClientToClipboard() { using (var bb = DisplayManager.RenderOffscreen(_currentVideoProvider, Config.ScreenshotCaptureOsd)) { using var img = bb.ToSysdrawingBitmap(); Clipboard.SetImage(img); } AddOnScreenMessage("Screenshot (client) saved to clipboard."); } public void TakeScreenshot() { var basename = $"{PathManager.ScreenshotPrefix(Game)}.{DateTime.Now:yyyy-MM-dd HH.mm.ss}"; var fnameBare = $"{basename}.png"; var fname = $"{basename} (0).png"; // if the (0) filename exists, do nothing. we'll bump up the number later // if the bare filename exists, move it to (0) // otherwise, no related filename exists, and we can proceed with the bare filename if (!File.Exists(fname)) { if (File.Exists(fnameBare)) File.Move(fnameBare, fname); else fname = fnameBare; } for (var seq = 0; File.Exists(fname); seq++) fname = $"{basename} ({seq}).png"; TakeScreenshot(fname); } public void TakeScreenshot(string path) { var fi = new FileInfo(path); if (fi.Directory != null && !fi.Directory.Exists) { fi.Directory.Create(); } using (var bb = Config.ScreenshotCaptureOsd ? CaptureOSD() : MakeScreenshotImage()) { using var img = bb.ToSysdrawingBitmap(); img.Save(fi.FullName, ImageFormat.Png); } AddOnScreenMessage($"{fi.Name} saved."); } public void FrameBufferResized() { // run this entire thing exactly twice, since the first resize may adjust the menu stacking for (int i = 0; i < 2; i++) { int zoom = Config.TargetZoomFactors[Emulator.SystemId]; var area = Screen.FromControl(this).WorkingArea; int borderWidth = Size.Width - PresentationPanel.Control.Size.Width; int borderHeight = Size.Height - PresentationPanel.Control.Size.Height; // start at target zoom and work way down until we find acceptable zoom Size lastComputedSize = new Size(1, 1); for (; zoom >= 1; zoom--) { lastComputedSize = DisplayManager.CalculateClientSize(_currentVideoProvider, zoom); if (lastComputedSize.Width + borderWidth < area.Width && lastComputedSize.Height + borderHeight < area.Height) { break; } } Console.WriteLine($"Selecting display size {lastComputedSize}"); // Change size Size = new Size(lastComputedSize.Width + borderWidth, lastComputedSize.Height + borderHeight); PerformLayout(); PresentationPanel.Resized = true; // Is window off the screen at this size? if (!area.Contains(Bounds)) { if (Bounds.Right > area.Right) // Window is off the right edge { Location = new Point(area.Right - Size.Width, Location.Y); } if (Bounds.Bottom > area.Bottom) // Window is off the bottom edge { Location = new Point(Location.X, area.Bottom - Size.Height); } } } } private void SynchChrome() { if (_inFullscreen) { // TODO - maybe apply a hack tracked during fullscreen here to override it FormBorderStyle = FormBorderStyle.None; MainMenuStrip.Visible = Config.DispChromeMenuFullscreen && !_argParser._chromeless; MainStatusBar.Visible = Config.DispChromeStatusBarFullscreen && !_argParser._chromeless; } else { MainStatusBar.Visible = Config.DispChromeStatusBarWindowed && !_argParser._chromeless; MainMenuStrip.Visible = Config.DispChromeMenuWindowed && !_argParser._chromeless; MaximizeBox = MinimizeBox = Config.DispChromeCaptionWindowed && !_argParser._chromeless; if (Config.DispChromeFrameWindowed == 0 || _argParser._chromeless) { FormBorderStyle = FormBorderStyle.None; } else if (Config.DispChromeFrameWindowed == 1) { FormBorderStyle = FormBorderStyle.SizableToolWindow; } else if (Config.DispChromeFrameWindowed == 2) { FormBorderStyle = FormBorderStyle.Sizable; } } } public void ToggleFullscreen(bool allowSuppress = false) { AutohideCursor(false); // prohibit this operation if the current controls include LMouse if (allowSuppress) { if (Global.ActiveController.HasBinding("WMouse L")) { return; } } if (!_inFullscreen) { SuspendLayout(); // Work around an AMD driver bug in >= vista: // It seems windows will activate opengl fullscreen mode when a GL control is occupying the exact space of a screen (0,0 and dimensions=screensize) // AMD cards manifest a problem under these circumstances, flickering other monitors. // It isn't clear whether nvidia cards are failing to employ this optimization, or just not flickering. // (this could be determined with more work; other side affects of the fullscreen mode include: corrupted TaskBar, no modal boxes on top of GL control, no screenshots) // At any rate, we can solve this by adding a 1px black border around the GL control // Please note: It is important to do this before resizing things, otherwise momentarily a GL control without WS_BORDER will be at the magic dimensions and cause the flakeout if (!OSTailoredCode.IsUnixHost && Config.DispFullscreenHacks && Config.DispMethod == EDispMethod.OpenGL) { // ATTENTION: this causes the StatusBar to not work well, since the backcolor is now set to black instead of SystemColors.Control. // It seems that some StatusBar elements composite with the backcolor. // Maybe we could add another control under the StatusBar. with a different backcolor Padding = new Padding(1); BackColor = Color.Black; // FUTURE WORK: // re-add this padding back into the display manager (so the image will get cut off a little but, but a few more resolutions will fully fit into the screen) } _windowedLocation = Location; _inFullscreen = true; SynchChrome(); WindowState = FormWindowState.Maximized; // be sure to do this after setting the chrome, otherwise it wont work fully ResumeLayout(); PresentationPanel.Resized = true; } else { SuspendLayout(); WindowState = FormWindowState.Normal; if (!OSTailoredCode.IsUnixHost) { // do this even if DispFullscreenHacks aren't enabled, to restore it in case it changed underneath us or something Padding = new Padding(0); // it's important that we set the form color back to this, because the StatusBar icons blend onto the mainform, not onto the StatusBar-- // so we need the StatusBar and mainform backdrop color to match BackColor = SystemColors.Control; } _inFullscreen = false; SynchChrome(); Location = _windowedLocation; ResumeLayout(); FrameBufferResized(); } } private void OpenLuaConsole() { Tools.Load(); } public void ClickSpeedItem(int num) { if ((ModifierKeys & Keys.Control) != 0) { SetSpeedPercentAlternate(num); } else { SetSpeedPercent(num); } } public void Unthrottle() { _unthrottled = true; } public void Throttle() { _unthrottled = false; } private void ThrottleMessage() { string type = ":(none)"; if (Config.SoundThrottle) { type = ":Sound"; } if (Config.VSyncThrottle) { type = $":Vsync{(Config.VSync ? "[ena]" : "[dis]")}"; } if (Config.ClockThrottle) { type = ":Clock"; } string throttled = _unthrottled ? "Unthrottled" : "Throttled"; string msg = $"{throttled}{type} "; AddOnScreenMessage(msg); } public void FrameSkipMessage() { AddOnScreenMessage($"Frameskipping set to {Config.FrameSkip}"); } public void UpdateCheatStatus() { if (CheatList.ActiveCount > 0) { CheatStatusButton.ToolTipText = "Cheats are currently active"; CheatStatusButton.Image = Properties.Resources.Freeze; CheatStatusButton.Visible = true; } else { CheatStatusButton.ToolTipText = ""; CheatStatusButton.Image = Properties.Resources.Blank; CheatStatusButton.Visible = false; } } private void SNES_ToggleBg(int layer) { if (!(Emulator is LibsnesCore || Emulator is Snes9x) || !1.RangeTo(4).Contains(layer)) { return; } bool result = false; if (Emulator is LibsnesCore bsnes) { var s = bsnes.GetSettings(); switch (layer) { case 1: result = s.ShowBG1_0 = s.ShowBG1_1 ^= true; break; case 2: result = s.ShowBG2_0 = s.ShowBG2_1 ^= true; break; case 3: result = s.ShowBG3_0 = s.ShowBG3_1 ^= true; break; case 4: result = s.ShowBG4_0 = s.ShowBG4_1 ^= true; break; } bsnes.PutSettings(s); } else if (Emulator is Snes9x snes9X) { var s = snes9X.GetSettings(); switch (layer) { case 1: result = s.ShowBg0 ^= true; break; case 2: result = s.ShowBg1 ^= true; break; case 3: result = s.ShowBg2 ^= true; break; case 4: result = s.ShowBg3 ^= true; break; } snes9X.PutSettings(s); } AddOnScreenMessage($"BG {layer} Layer {(result ? "On" : "Off")}"); } private void SNES_ToggleObj(int layer) { if (!(Emulator is LibsnesCore || Emulator is Snes9x) || !1.RangeTo(4).Contains(layer)) { return; } bool result = false; if (Emulator is LibsnesCore bsnes) { var s = bsnes.GetSettings(); switch (layer) { case 1: result = s.ShowOBJ_0 ^= true; break; case 2: result = s.ShowOBJ_1 ^= true; break; case 3: result = s.ShowOBJ_2 ^= true; break; case 4: result = s.ShowOBJ_3 ^= true; break; } bsnes.PutSettings(s); AddOnScreenMessage($"Obj {layer} Layer {(result ? "On" : "Off")}"); } else if (Emulator is Snes9x snes9X) { var s = snes9X.GetSettings(); switch (layer) { case 1: result = s.ShowSprites0 ^= true; break; case 2: result = s.ShowSprites1 ^= true; break; case 3: result = s.ShowSprites2 ^= true; break; case 4: result = s.ShowSprites3 ^= true; break; } snes9X.PutSettings(s); AddOnScreenMessage($"Sprite {layer} Layer {(result ? "On" : "Off")}"); } } public bool RunLibretroCoreChooser() { using var ofd = new OpenFileDialog(); if (Config.LibretroCore != null) { ofd.FileName = Path.GetFileName(Config.LibretroCore); ofd.InitialDirectory = Path.GetDirectoryName(Config.LibretroCore); } else { ofd.InitialDirectory = PathManager.GetPathType("Libretro", "Cores"); if (!Directory.Exists(ofd.InitialDirectory)) { Directory.CreateDirectory(ofd.InitialDirectory); } } ofd.RestoreDirectory = true; ofd.Filter = "Libretro Cores (*.dll)|*.dll"; if (ofd.ShowDialog() == DialogResult.Cancel) { return false; } Config.LibretroCore = ofd.FileName; return true; } #endregion #region Private variables private Size _lastVideoSize = new Size(-1, -1), _lastVirtualSize = new Size(-1, -1); private readonly SaveSlotManager _stateSlots = new SaveSlotManager(); // AVI/WAV state private IVideoWriter _currAviWriter; private AutofireController _autofireNullControls; // Sound refactor TODO: we can enforce async mode here with a property that gets/sets this but does an async check private ISoundProvider _aviSoundInputAsync; // Note: This sound provider must be in async mode! private SimpleSyncSoundProvider _dumpProxy; // an audio proxy used for dumping private bool _dumpaudiosync; // set true to for experimental AV dumping private int _avwriterResizew; private int _avwriterResizeh; private bool _avwriterpad; private bool _windowClosedAndSafeToExitProcess; private int _exitCode; private bool _exitRequestPending; private bool _runloopFrameProgress; private long _frameAdvanceTimestamp; private long _frameRewindTimestamp; private bool _frameRewindWasPaused; private bool _runloopFrameAdvance; private bool _lastFastForwardingOrRewinding; private bool _inResizeLoop; private readonly double _fpsUpdatesPerSecond = 4.0; private readonly double _fpsSmoothing = 8.0; private double _lastFps; private int _framesSinceLastFpsUpdate; private long _timestampLastFpsUpdate; private readonly Throttle _throttle; private bool _unthrottled; // For handling automatic pausing when entering the menu private bool _wasPaused; private bool _didMenuPause; private bool _cursorHidden; private bool _inFullscreen; private Point _windowedLocation; private bool _needsFullscreenOnLoad; private int _lastOpenRomFilter; private readonly ArgParser _argParser = new ArgParser(); // Resources private Bitmap _statusBarDiskLightOnImage; private Bitmap _statusBarDiskLightOffImage; private Bitmap _linkCableOn; private Bitmap _linkCableOff; // input state which has been destined for game controller inputs are coalesced here // public static ControllerInputCoalescer ControllerInputCoalescer = new ControllerInputCoalescer(); // input state which has been destined for client hotkey consumption are colesced here private readonly InputCoalescer _hotkeyCoalescer = new InputCoalescer(); public PresentationPanel PresentationPanel { get; } // countdown for saveram autoflushing public int AutoFlushSaveRamIn { get; set; } #endregion #region Private methods private void SetStatusBar() { if (!_inFullscreen) { MainStatusBar.Visible = Config.DispChromeStatusBarWindowed; PerformLayout(); FrameBufferResized(); } } public void SetWindowText() { string str = ""; if (_inResizeLoop) { var size = PresentationPanel.NativeSize; float ar = (float)size.Width / size.Height; str += $"({size.Width}x{size.Height})={ar} - "; } // we need to display FPS somewhere, in this case if (Config.DispSpeedupFeatures == 0) { str += $"({_lastFps:0} fps) - "; } if (!string.IsNullOrEmpty(VersionInfo.CustomBuildString)) { str += $"{VersionInfo.CustomBuildString} "; } str += Emulator.IsNull() ? "BizHawk" : Emulator.System().DisplayName; if (VersionInfo.DeveloperBuild) { str += " (interim)"; } if (!Emulator.IsNull()) { str += $" - {Game.Name}"; if (MovieSession.Movie.IsActive()) { str += $" - {Path.GetFileName(MovieSession.Movie.Filename)}"; } } if (!Config.DispChromeCaptionWindowed || _argParser._chromeless) { str = ""; } Text = str; } private void ClearAutohold() { ClearHolds(); AddOnScreenMessage("Autohold keys cleared"); } private void UpdateToolsLoadstate() { if (Tools.Has()) { Tools.SNESGraphicsDebugger.UpdateToolsLoadstate(); } } private void UpdateToolsAfter(bool fromLua = false) { Tools.UpdateToolsAfter(fromLua); HandleToggleLightAndLink(); } public void UpdateDumpIcon() { DumpStatusButton.Image = Properties.Resources.Blank; DumpStatusButton.ToolTipText = ""; if (Emulator.IsNull() || Game == null) { return; } var status = Game.Status; string annotation; if (status == RomStatus.BadDump) { DumpStatusButton.Image = Properties.Resources.ExclamationRed; annotation = "Warning: Bad ROM Dump"; } else if (status == RomStatus.Overdump) { DumpStatusButton.Image = Properties.Resources.ExclamationRed; annotation = "Warning: Overdump"; } else if (status == RomStatus.NotInDatabase) { DumpStatusButton.Image = Properties.Resources.RetroQuestion; annotation = "Warning: Unknown ROM"; } else if (status == RomStatus.TranslatedRom) { DumpStatusButton.Image = Properties.Resources.Translation; annotation = "Translated ROM"; } else if (status == RomStatus.Homebrew) { DumpStatusButton.Image = Properties.Resources.HomeBrew; annotation = "Homebrew ROM"; } else if (Game.Status == RomStatus.Hack) { DumpStatusButton.Image = Properties.Resources.Hack; annotation = "Hacked ROM"; } else if (Game.Status == RomStatus.Unknown) { DumpStatusButton.Image = Properties.Resources.Hack; annotation = "Warning: ROM of Unknown Character"; } else { DumpStatusButton.Image = Properties.Resources.GreenCheck; annotation = "Verified good dump"; } if (!string.IsNullOrEmpty(Emulator.CoreComm.RomStatusAnnotation)) { annotation = Emulator.CoreComm.RomStatusAnnotation; if (annotation == "Multi-disk bundler") { DumpStatusButton.Image = Properties.Resources.RetroQuestion; } } DumpStatusButton.ToolTipText = annotation; } private void LoadSaveRam() { if (Emulator.HasSaveRam()) { try // zero says: this is sort of sketchy... but this is no time for rearchitecting { if (Config.AutosaveSaveRAM) { var saveram = new FileInfo(PathManager.SaveRamPath(Game)); var autosave = new FileInfo(PathManager.AutoSaveRamPath(Game)); if (autosave.Exists && autosave.LastWriteTime > saveram.LastWriteTime) { AddOnScreenMessage("AutoSaveRAM is newer than last saved SaveRAM"); } } byte[] sram; // GBA meteor core might not know how big the saveram ought to be, so just send it the whole file // GBA vba-next core will try to eat anything, regardless of size if (Emulator is VBANext || Emulator is MGBAHawk || Emulator is NeoGeoPort) { sram = File.ReadAllBytes(PathManager.SaveRamPath(Game)); } else { var oldRam = Emulator.AsSaveRam().CloneSaveRam(); if (oldRam == null) { // we're eating this one now. The possible negative consequence is that a user could lose // their saveram and not know why // MessageBox.Show("Error: tried to load saveram, but core would not accept it?"); return; } // why do we silently truncate\pad here instead of warning\erroring? sram = new byte[oldRam.Length]; using var reader = new BinaryReader( new FileStream(PathManager.SaveRamPath(Game), FileMode.Open, FileAccess.Read)); reader.Read(sram, 0, sram.Length); } Emulator.AsSaveRam().StoreSaveRam(sram); AutoFlushSaveRamIn = Config.FlushSaveRamFrames; } catch (IOException) { AddOnScreenMessage("An error occurred while loading Sram"); } } } public bool FlushSaveRAM(bool autosave = false) { if (Emulator.HasSaveRam()) { string path; if (autosave) { path = PathManager.AutoSaveRamPath(Game); AutoFlushSaveRamIn = Config.FlushSaveRamFrames; } else { path = PathManager.SaveRamPath(Game); } var file = new FileInfo(path); var newPath = $"{path}.new"; var newFile = new FileInfo(newPath); var backupPath = $"{path}.bak"; var backupFile = new FileInfo(backupPath); if (file.Directory != null && !file.Directory.Exists) { try { file.Directory.Create(); } catch { AddOnScreenMessage($"Unable to flush SaveRAM to: {newFile.Directory}"); return false; } } using (var writer = new BinaryWriter(new FileStream(newPath, FileMode.Create, FileAccess.Write))) { var saveram = Emulator.AsSaveRam().CloneSaveRam(); if (saveram != null) { writer.Write(saveram, 0, saveram.Length); } } if (file.Exists) { if (Config.BackupSaveram) { if (backupFile.Exists) { backupFile.Delete(); } file.MoveTo(backupPath); } else { file.Delete(); } } newFile.MoveTo(path); } return true; } private void RewireSound() { if (_dumpProxy != null) { // we're video dumping, so async mode only and use the DumpProxy. // note that the avi dumper has already rewired the emulator itself in this case. Sound.SetInputPin(_dumpProxy); } else { bool useAsyncMode = _currentSoundProvider.CanProvideAsync && !Config.SoundThrottle; _currentSoundProvider.SetSyncMode(useAsyncMode ? SyncSoundMode.Async : SyncSoundMode.Sync); Sound.SetInputPin(_currentSoundProvider); } } private void HandlePlatformMenus() { var system = ""; if (!Game.IsNullInstance()) { system = Emulator.SystemId; } TI83SubMenu.Visible = false; NESSubMenu.Visible = false; PCESubMenu.Visible = false; SMSSubMenu.Visible = false; GBSubMenu.Visible = false; GBASubMenu.Visible = false; AtariSubMenu.Visible = false; A7800SubMenu.Visible = false; SNESSubMenu.Visible = false; PSXSubMenu.Visible = false; ColecoSubMenu.Visible = false; N64SubMenu.Visible = false; SaturnSubMenu.Visible = false; DGBSubMenu.Visible = false; DGBHawkSubMenu.Visible = false; GB3xSubMenu.Visible = false; GB4xSubMenu.Visible = false; GGLSubMenu.Visible = false; GenesisSubMenu.Visible = false; wonderSwanToolStripMenuItem.Visible = false; AppleSubMenu.Visible = false; C64SubMenu.Visible = false; IntvSubMenu.Visible = false; virtualBoyToolStripMenuItem.Visible = false; sNESToolStripMenuItem.Visible = false; neoGeoPocketToolStripMenuItem.Visible = false; pCFXToolStripMenuItem.Visible = false; zXSpectrumToolStripMenuItem.Visible = false; amstradCPCToolStripMenuItem.Visible = false; VectrexSubMenu.Visible = false; MSXSubMenu.Visible = false; O2HawkSubMenu.Visible = false; arcadeToolStripMenuItem.Visible = false; switch (system) { case "GEN": GenesisSubMenu.Visible = true; break; case "TI83": TI83SubMenu.Visible = true; break; case "NES": NESSubMenu.Visible = true; break; case "PCE": case "PCECD": case "SGX": PCESubMenu.Visible = true; break; case "SMS": SMSSubMenu.Text = "&SMS"; SMSSubMenu.Visible = true; break; case "SG": SMSSubMenu.Text = "&SG"; SMSSubMenu.Visible = true; break; case "GG": SMSSubMenu.Text = "&GG"; SMSSubMenu.Visible = true; break; case "GB": case "GBC": GBSubMenu.Visible = true; break; case "GBA": GBASubMenu.Visible = true; break; case "A26": AtariSubMenu.Visible = true; break; case "A78": A7800SubMenu.Visible = true; break; case "PSX": PSXSubMenu.Visible = true; break; case "SNES": case "SGB": if (Emulator is LibsnesCore) { SNESSubMenu.Text = ((LibsnesCore)Emulator).IsSGB ? "&SGB" : "&SNES"; SNESSubMenu.Visible = true; } else if (Emulator is Snes9x) { sNESToolStripMenuItem.Visible = true; } else if (Emulator is Sameboy) { GBSubMenu.Visible = true; } break; case "Coleco": ColecoSubMenu.Visible = true; break; case "N64": N64SubMenu.Visible = true; break; case "SAT": SaturnSubMenu.Visible = true; break; case "DGB": if (Emulator is GBHawkLink) { DGBHawkSubMenu.Visible = true; } else { DGBSubMenu.Visible = true; } break; case "WSWAN": wonderSwanToolStripMenuItem.Visible = true; break; case "AppleII": AppleSubMenu.Visible = true; break; case "C64": C64SubMenu.Visible = true; break; case "INTV": IntvSubMenu.Visible = true; break; case "VB": virtualBoyToolStripMenuItem.Visible = true; break; case "NGP": neoGeoPocketToolStripMenuItem.Visible = true; break; case "PCFX": pCFXToolStripMenuItem.Visible = true; break; case "ZXSpectrum": zXSpectrumToolStripMenuItem.Visible = true; #if DEBUG ZXSpectrumExportSnapshotMenuItemMenuItem.Visible = true; #else ZXSpectrumExportSnapshotMenuItemMenuItem.Visible = false; #endif break; case "AmstradCPC": amstradCPCToolStripMenuItem.Visible = true; break; case "GGL": GGLSubMenu.Visible = true; break; case "VEC": VectrexSubMenu.Visible = true; break; case "MSX": MSXSubMenu.Visible = true; break; case "O2": O2HawkSubMenu.Visible = true; break; case "GB3x": GB3xSubMenu.Visible = true; break; case "GB4x": GB4xSubMenu.Visible = true; break; case "MAME": arcadeToolStripMenuItem.Visible = true; break; } } private void InitControls() { var controls = new Controller( new ControllerDefinition { Name = "Emulator Frontend Controls", BoolButtons = Config.HotkeyBindings.Select(x => x.DisplayName).ToList() }); foreach (var b in Config.HotkeyBindings) { controls.BindMulti(b.DisplayName, b.Bindings); } Global.ClientControls = controls; _autofireNullControls = new AutofireController(NullController.Instance.Definition, Emulator); } private void LoadMoviesFromRecent(string path) { if (File.Exists(path)) { var movie = MovieService.Get(path); MovieSession.ReadOnly = true; StartNewMovie(movie, false); } else { Config.RecentMovies.HandleLoadError(path); } } private void LoadRomFromRecent(string rom) { var ioa = OpenAdvancedSerializer.ParseWithLegacy(rom); var args = new LoadRomArgs { OpenAdvanced = ioa }; // if(ioa is this or that) - for more complex behaviour string romPath = ioa.SimplePath; if (!LoadRom(romPath, args)) { Config.RecentRoms.HandleLoadError(romPath, rom); } } private void SetPauseStatusBarIcon() { if (EmulatorPaused) { PauseStatusButton.Image = Properties.Resources.Pause; PauseStatusButton.Visible = true; PauseStatusButton.ToolTipText = "Emulator Paused"; } else if (IsTurboSeeking) { PauseStatusButton.Image = Properties.Resources.Lightning; PauseStatusButton.Visible = true; // ReSharper disable once PossibleInvalidOperationException PauseStatusButton.ToolTipText = $"Emulator is turbo seeking to frame {PauseOnFrame.Value} click to stop seek"; } else if (PauseOnFrame.HasValue) { PauseStatusButton.Image = Properties.Resources.YellowRight; PauseStatusButton.Visible = true; PauseStatusButton.ToolTipText = $"Emulator is playing to frame {PauseOnFrame.Value} click to stop seek"; } else { PauseStatusButton.Image = Properties.Resources.Blank; PauseStatusButton.Visible = false; PauseStatusButton.ToolTipText = ""; } } private void SyncThrottle() { // "unthrottled" = throttle was turned off with "Toggle Throttle" hotkey // "turbo" = throttle is off due to the "Turbo" hotkey being held // They are basically the same thing but one is a toggle and the other requires a // hotkey to be held. There is however slightly different behavior in that turbo // skips outputting the audio. There's also a third way which is when no throttle // method is selected, but the clock throttle determines that by itself and // everything appears normal here. var rewind = Rewinder.RewindActive && (ClientControls["Rewind"] || PressRewind); var fastForward = ClientControls["Fast Forward"] || FastForward; var turbo = IsTurboing; int speedPercent = fastForward ? Config.SpeedPercentAlternate : Config.SpeedPercent; if (rewind) { speedPercent = Math.Max(speedPercent * Config.Rewind.SpeedMultiplier / Rewinder.RewindFrequency, 5); } Global.DisableSecondaryThrottling = _unthrottled || turbo || fastForward || rewind; // realtime throttle is never going to be so exact that using a double here is wrong _throttle.SetCoreFps(Emulator.VsyncRate()); _throttle.signal_paused = EmulatorPaused; _throttle.signal_unthrottle = _unthrottled || turbo; // zero 26-mar-2016 - vsync and vsync throttle here both is odd, but see comments elsewhere about triple buffering _throttle.signal_overrideSecondaryThrottle = (fastForward || rewind) && (Config.SoundThrottle || Config.VSyncThrottle || Config.VSync); _throttle.SetSpeedPercent(speedPercent); } private void SetSpeedPercentAlternate(int value) { Config.SpeedPercentAlternate = value; SyncThrottle(); AddOnScreenMessage($"Alternate Speed: {value}%"); } private void SetSpeedPercent(int value) { Config.SpeedPercent = value; SyncThrottle(); AddOnScreenMessage($"Speed: {value}%"); } private void Shutdown() { if (_currAviWriter != null) { _currAviWriter.CloseFile(); _currAviWriter = null; } } private static void CheckMessages() { Application.DoEvents(); if (ActiveForm != null) { ScreenSaver.ResetTimerPeriodically(); } } private void AutohideCursor(bool hide) { if (hide && !_cursorHidden) { PresentationPanel.Control.Cursor = Properties.Resources.BlankCursor; _cursorHidden = true; } else if (!hide && _cursorHidden) { PresentationPanel.Control.Cursor = Cursors.Default; timerMouseIdle.Stop(); timerMouseIdle.Start(); _cursorHidden = false; } } public BitmapBuffer MakeScreenshotImage() { return DisplayManager.RenderVideoProvider(_currentVideoProvider); } private void SaveSlotSelectedMessage() { int slot = Config.SaveSlot; string emptyPart = _stateSlots.HasSlot(slot) ? "" : " (empty)"; string message = $"Slot {slot}{emptyPart} selected."; AddOnScreenMessage(message); } private void Render() { if (Config.DispSpeedupFeatures == 0) { return; } var video = _currentVideoProvider; Size currVideoSize = new Size(video.BufferWidth, video.BufferHeight); Size currVirtualSize = new Size(video.VirtualWidth, video.VirtualHeight); bool resizeFramebuffer = currVideoSize != _lastVideoSize || currVirtualSize != _lastVirtualSize; bool isZero = currVideoSize.Width == 0 || currVideoSize.Height == 0 || currVirtualSize.Width == 0 || currVirtualSize.Height == 0; //don't resize if the new size is 0 somehow; we'll wait until we have a sensible size if (isZero) { resizeFramebuffer = false; } if (resizeFramebuffer) { _lastVideoSize = currVideoSize; _lastVirtualSize = currVirtualSize; FrameBufferResized(); } //rendering flakes out egregiously if we have a zero size //can we fix it later not to? if (isZero) DisplayManager.Blank(); else DisplayManager.UpdateSource(video); } // sends a simulation of a plain alt key keystroke private void SendPlainAltKey(int lparam) { var m = new Message { WParam = new IntPtr(0xF100), LParam = new IntPtr(lparam), Msg = 0x0112, HWnd = Handle }; base.WndProc(ref m); } // sends an alt+mnemonic combination private void SendAltKeyChar(char c) { switch (OSTailoredCode.CurrentOS) { case OSTailoredCode.DistinctOS.Linux: case OSTailoredCode.DistinctOS.macOS: // no mnemonics for you break; case OSTailoredCode.DistinctOS.Windows: //HACK var _ = typeof(ToolStrip).InvokeMember( "ProcessMnemonicInternal", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.InvokeMethod | System.Reflection.BindingFlags.Instance, null, MainformMenu, new object[] { c }); break; } } public static string ToFilter(string name, IDictionary entries) { var items = new List { name, string.Join(";", entries.Select(e => $"*{e.Value}")) }; foreach (var kvp in entries) { items.Add(kvp.Key); items.Add($"*{kvp.Value}"); } items.Add("All Files"); items.Add("*.*"); return FormatFilter(items.ToArray()); } /// contains unpaired element public static string FormatFilter(params string[] args) { var sb = new StringBuilder(); if (args.Length % 2 != 0) { throw new ArgumentException(); } var num = args.Length / 2; for (int i = 0; i < num; i++) { sb.AppendFormat("{0} ({1})|{1}", args[i * 2], args[(i * 2) + 1]); if (i != num - 1) { sb.Append('|'); } } var str = sb.ToString().Replace("%ARCH%", ArchiveFilters); str = str.Replace(";", "; "); return str; } public static FileFilterEntry[] RomFilterEntries { get; } = { new FileFilterEntry("Music Files", null, developerFilters: "*.psf;*.minipsf;*.sid;*.nsf"), new FileFilterEntry("Disc Images", "*.cue;*.ccd;*.mds;*.m3u"), new FileFilterEntry("NES", "*.nes;*.fds;*.unf;*.nsf;%ARCH%"), new FileFilterEntry("Super NES", "*.smc;*.sfc;*.xml;%ARCH%"), new FileFilterEntry("PlayStation", "*.cue;*.ccd;*.mds;*.m3u"), new FileFilterEntry("PSX Executables (experimental)", null, developerFilters: "*.exe"), new FileFilterEntry("PSF Playstation Sound File", "*.psf;*.minipsf"), new FileFilterEntry("Nintendo 64", "*.z64;*.v64;*.n64"), new FileFilterEntry("Gameboy", "*.gb;*.gbc;*.sgb;%ARCH%"), new FileFilterEntry("Gameboy Advance", "*.gba;%ARCH%"), new FileFilterEntry("Master System", "*.sms;*.gg;*.sg;%ARCH%"), new FileFilterEntry("PC Engine", "*.pce;*.sgx;*.cue;*.ccd;*.mds;%ARCH%"), new FileFilterEntry("Atari 2600", "*.a26;%ARCH%", developerFilters: "*.bin"), new FileFilterEntry("Atari 7800", "*.a78;%ARCH%", developerFilters: "*.bin"), new FileFilterEntry("Atari Lynx", "*.lnx;%ARCH%"), new FileFilterEntry("ColecoVision", "*.col;%ARCH%"), new FileFilterEntry("IntelliVision", "*.int;*.bin;*.rom;%ARCH%"), new FileFilterEntry("TI-83", "*.rom;%ARCH%"), new FileFilterEntry("Archive Files", "%ARCH%"), new FileFilterEntry("Genesis", "*.gen;*.md;*.smd;*.32x;*.bin;*.cue;*.ccd;%ARCH%"), new FileFilterEntry("SID Commodore 64 Music File", null, developerFilters: "*.sid;%ARCH%"), new FileFilterEntry("WonderSwan", "*.ws;*.wsc;%ARCH%"), new FileFilterEntry("Apple II", "*.dsk;*.do;*.po;%ARCH%"), new FileFilterEntry("Virtual Boy", "*.vb;%ARCH%"), new FileFilterEntry("Neo Geo Pocket", "*.ngp;*.ngc;%ARCH%"), new FileFilterEntry("Commodore 64", "*.prg;*.d64;*.g64;*.crt;*.tap;%ARCH%"), new FileFilterEntry("Amstrad CPC", null, developerFilters: "*.cdt;*.dsk;%ARCH%"), new FileFilterEntry("Sinclair ZX Spectrum", "*.tzx;*.tap;*.dsk;*.pzx;*.csw;*.wav;%ARCH%") }; public const string ArchiveFilters = "*.zip;*.rar;*.7z;*.gz"; public static string RomFilter { get { string GetRomFilterStrings() { var values = new HashSet(RomFilterEntries.SelectMany(f => f.EffectiveFilters)); if (values.Remove("%ARCH%")) { values.UnionWith(ArchiveFilters.Split(';')); } return string.Join(";", values.OrderBy(n => n)); } var allFilters = new List { new FileFilterEntry("Rom Files", GetRomFilterStrings()) }; allFilters.AddRange(RomFilterEntries.Where(f => f.EffectiveFilters.Any())); allFilters.Add(new FileFilterEntry("Savestate", "*.state")); allFilters.Add(new FileFilterEntry("All Files", "*.*")); return FormatFilter(allFilters.SelectMany(f => new[] { f.Description, string.Join(";", f.EffectiveFilters) }).ToArray()); } } private void OpenRom() { using var ofd = new OpenFileDialog { InitialDirectory = PathManager.GetRomsPath(Emulator.SystemId), Filter = RomFilter, RestoreDirectory = false, FilterIndex = _lastOpenRomFilter }; var result = ofd.ShowHawkDialog(); if (result != DialogResult.OK) { return; } var file = new FileInfo(ofd.FileName); _lastOpenRomFilter = ofd.FilterIndex; var lra = new LoadRomArgs { OpenAdvanced = new OpenAdvanced_OpenRom { Path = file.FullName } }; LoadRom(file.FullName, lra); } private void CoreSyncSettings(object sender, RomLoader.SettingsLoadArgs e) { if (MovieSession.QueuedMovie != null) { if (!string.IsNullOrWhiteSpace(MovieSession.QueuedMovie.SyncSettingsJson)) { e.Settings = ConfigService.LoadWithType(MovieSession.QueuedMovie.SyncSettingsJson); } else { e.Settings = Config.GetCoreSyncSettings(e.Core); // Only show this nag if the core actually has sync settings, not all cores do if (e.Settings != null && !_suppressSyncSettingsWarning) { MessageBox.Show( "No sync settings found, using currently configured settings for this core.", "No sync settings found", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } } else { e.Settings = Config.GetCoreSyncSettings(e.Core); } } private void CoreSettings(object sender, RomLoader.SettingsLoadArgs e) { e.Settings = Config.GetCoreSettings(e.Core); } /// /// send core settings to emu, setting reboot flag if needed /// public void PutCoreSettings(object o) { var settable = new SettingsAdapter(Emulator); if (settable.HasSettings && settable.PutSettings(o)) { FlagNeedsReboot(); } } // TODO: Get/Put settings/sync settings methods could become a service we instantiate and use and pass to other forms /// /// send core sync settings to emu, setting reboot flag if needed /// public void PutCoreSyncSettings(object o) { var settable = new SettingsAdapter(Emulator); if (MovieSession.Movie.IsActive()) { AddOnScreenMessage("Attempt to change sync-relevant settings while recording BLOCKED."); } else if (settable.HasSyncSettings && settable.PutSyncSettings(o)) { FlagNeedsReboot(); } } private void SaveConfig(string path = "") { if (Config.SaveWindowPosition) { if (Config.MainWndx != -32000) // When minimized location is -32000, don't save this into the config file! { Config.MainWndx = Location.X; } if (Config.MainWndy != -32000) { Config.MainWndy = Location.Y; } } else { Config.MainWndx = -1; Config.MainWndy = -1; } if (string.IsNullOrEmpty(path)) { path = PathManager.DefaultIniPath; } ConfigService.Save(path, Config); } private void ToggleFps() { Config.DisplayFps ^= true; } private void ToggleFrameCounter() { Config.DisplayFrameCounter ^= true; } private void ToggleLagCounter() { Config.DisplayLagCounter ^= true; } private void ToggleInputDisplay() { Config.DisplayInput ^= true; } private void ToggleSound() { Config.SoundEnabled ^= true; Sound.StopSound(); Sound.StartSound(); } private void VolumeUp() { Config.SoundVolume += 10; if (Config.SoundVolume > 100) { Config.SoundVolume = 100; } AddOnScreenMessage($"Volume {Config.SoundVolume}"); } private void VolumeDown() { Config.SoundVolume -= 10; if (Config.SoundVolume < 0) { Config.SoundVolume = 0; } AddOnScreenMessage($"Volume {Config.SoundVolume}"); } private void SoftReset() { // is it enough to run this for one frame? maybe.. if (Emulator.ControllerDefinition.BoolButtons.Contains("Reset")) { if (MovieSession.Movie.Mode != MovieMode.Play) { ClickyVirtualPadController.Click("Reset"); AddOnScreenMessage("Reset button pressed."); } } } private void HardReset() { // is it enough to run this for one frame? maybe.. if (Emulator.ControllerDefinition.BoolButtons.Contains("Power")) { if (MovieSession.Movie.Mode != MovieMode.Play) { ClickyVirtualPadController.Click("Power"); AddOnScreenMessage("Power button pressed."); } } } private Color SlotForeColor(int slot) { return _stateSlots.HasSlot(slot) ? Config.SaveSlot == slot ? SystemColors.HighlightText : SystemColors.WindowText : SystemColors.GrayText; } private Color SlotBackColor(int slot) { return Config.SaveSlot == slot ? SystemColors.Highlight : SystemColors.Control; } public void UpdateStatusSlots() { _stateSlots.Update(); Slot0StatusButton.ForeColor = SlotForeColor(0); Slot1StatusButton.ForeColor = SlotForeColor(1); Slot2StatusButton.ForeColor = SlotForeColor(2); Slot3StatusButton.ForeColor = SlotForeColor(3); Slot4StatusButton.ForeColor = SlotForeColor(4); Slot5StatusButton.ForeColor = SlotForeColor(5); Slot6StatusButton.ForeColor = SlotForeColor(6); Slot7StatusButton.ForeColor = SlotForeColor(7); Slot8StatusButton.ForeColor = SlotForeColor(8); Slot9StatusButton.ForeColor = SlotForeColor(9); Slot0StatusButton.BackColor = SlotBackColor(0); Slot1StatusButton.BackColor = SlotBackColor(1); Slot2StatusButton.BackColor = SlotBackColor(2); Slot3StatusButton.BackColor = SlotBackColor(3); Slot4StatusButton.BackColor = SlotBackColor(4); Slot5StatusButton.BackColor = SlotBackColor(5); Slot6StatusButton.BackColor = SlotBackColor(6); Slot7StatusButton.BackColor = SlotBackColor(7); Slot8StatusButton.BackColor = SlotBackColor(8); Slot9StatusButton.BackColor = SlotBackColor(9); SaveSlotsStatusLabel.Visible = Slot0StatusButton.Visible = Slot1StatusButton.Visible = Slot2StatusButton.Visible = Slot3StatusButton.Visible = Slot4StatusButton.Visible = Slot5StatusButton.Visible = Slot6StatusButton.Visible = Slot7StatusButton.Visible = Slot8StatusButton.Visible = Slot9StatusButton.Visible = Emulator.HasSavestates(); } public BitmapBuffer CaptureOSD() { var bb = DisplayManager.RenderOffscreen(_currentVideoProvider, true); bb.DiscardAlpha(); return bb; } private void IncreaseWindowSize() { switch (Config.TargetZoomFactors[Emulator.SystemId]) { case 1: Config.TargetZoomFactors[Emulator.SystemId] = 2; break; case 2: Config.TargetZoomFactors[Emulator.SystemId] = 3; break; case 3: Config.TargetZoomFactors[Emulator.SystemId] = 4; break; case 4: Config.TargetZoomFactors[Emulator.SystemId] = 5; break; case 5: Config.TargetZoomFactors[Emulator.SystemId] = 10; break; case 10: return; } AddOnScreenMessage($"Screensize set to {Config.TargetZoomFactors[Emulator.SystemId]}x"); FrameBufferResized(); } private void DecreaseWindowSize() { switch (Config.TargetZoomFactors[Emulator.SystemId]) { case 1: return; case 2: Config.TargetZoomFactors[Emulator.SystemId] = 1; break; case 3: Config.TargetZoomFactors[Emulator.SystemId] = 2; break; case 4: Config.TargetZoomFactors[Emulator.SystemId] = 3; break; case 5: Config.TargetZoomFactors[Emulator.SystemId] = 4; break; case 10: Config.TargetZoomFactors[Emulator.SystemId] = 5; return; } AddOnScreenMessage($"Screensize set to {Config.TargetZoomFactors[Emulator.SystemId]}x"); FrameBufferResized(); } private static readonly int[] SpeedPercents = { 1, 3, 6, 12, 25, 50, 75, 100, 150, 200, 300, 400, 800, 1600, 3200, 6400 }; private void IncreaseSpeed() { if (!Config.ClockThrottle) { AddOnScreenMessage("Unable to change speed, please switch to clock throttle"); return; } var oldPercent = Config.SpeedPercent; int newPercent; int i = 0; do { i++; newPercent = SpeedPercents[i]; } while (newPercent <= oldPercent && i < SpeedPercents.Length - 1); SetSpeedPercent(newPercent); } private void DecreaseSpeed() { if (!Config.ClockThrottle) { AddOnScreenMessage("Unable to change speed, please switch to clock throttle"); return; } var oldPercent = Config.SpeedPercent; int newPercent; int i = SpeedPercents.Length - 1; do { i--; newPercent = SpeedPercents[i]; } while (newPercent >= oldPercent && i > 0); SetSpeedPercent(newPercent); } private void SaveMovie() { if (MovieSession.Movie.IsActive()) { MovieSession.Movie.Save(); AddOnScreenMessage($"{MovieSession.Movie.Filename} saved."); } } private void HandleToggleLightAndLink() { if (MainStatusBar.Visible) { var hasDriveLight = Emulator.HasDriveLight() && Emulator.AsDriveLight().DriveLightEnabled; if (hasDriveLight) { if (!LedLightStatusLabel.Visible) { LedLightStatusLabel.Visible = true; } LedLightStatusLabel.Image = Emulator.AsDriveLight().DriveLightOn ? _statusBarDiskLightOnImage : _statusBarDiskLightOffImage; } else { if (LedLightStatusLabel.Visible) { LedLightStatusLabel.Visible = false; } } if (Emulator.UsesLinkCable()) { if (!LinkConnectStatusBarButton.Visible) { LinkConnectStatusBarButton.Visible = true; } LinkConnectStatusBarButton.Image = Emulator.AsLinkable().LinkConnected ? _linkCableOn : _linkCableOff; LinkConnectStatusBarButton.ToolTipText = $"Link connection is currently {(Emulator.AsLinkable().LinkConnected ? "enabled" : "disabled")}"; } else { if (LinkConnectStatusBarButton.Visible) { LinkConnectStatusBarButton.Visible = false; } } } } private void UpdateKeyPriorityIcon() { switch (Config.InputHotkeyOverrideOptions) { default: case 0: KeyPriorityStatusLabel.Image = Properties.Resources.Both; KeyPriorityStatusLabel.ToolTipText = "Key priority: Allow both hotkeys and controller buttons"; break; case 1: KeyPriorityStatusLabel.Image = Properties.Resources.GameController; KeyPriorityStatusLabel.ToolTipText = "Key priority: Controller buttons will override hotkeys"; break; case 2: KeyPriorityStatusLabel.Image = Properties.Resources.HotKeys; KeyPriorityStatusLabel.ToolTipText = "Key priority: Hotkeys will override controller buttons"; break; } } private void ToggleModePokeMode() { Config.MoviePlaybackPokeMode ^= true; AddOnScreenMessage($"Movie Poke mode {(Config.MoviePlaybackPokeMode ? "enabled" : "disabled")}"); } private void ToggleBackgroundInput() { Config.AcceptBackgroundInput ^= true; AddOnScreenMessage($"Background Input {(Config.AcceptBackgroundInput ? "enabled" : "disabled")}"); } private void VsyncMessage() { AddOnScreenMessage($"Display Vsync set to {(Config.VSync ? "on" : "off")}"); } private static bool StateErrorAskUser(string title, string message) { var result = MessageBox.Show( message, title, MessageBoxButtons.YesNo, MessageBoxIcon.Question); return result == DialogResult.Yes; } private void FdsInsertDiskMenuAdd(string name, string button, string msg) { FDSControlsMenuItem.DropDownItems.Add(name, null, delegate { if (Emulator.ControllerDefinition.BoolButtons.Contains(button)) { if (MovieSession.Movie.Mode != MovieMode.Play) { ClickyVirtualPadController.Click(button); AddOnScreenMessage(msg); } } }); } private const int WmDeviceChange = 0x0219; // Alt key hacks protected override void WndProc(ref Message m) { switch (m.Msg) { case WmDeviceChange: GamePad.Initialize(this); GamePad360.Initialize(); break; } // this is necessary to trap plain alt keypresses so that only our hotkey system gets them if (m.Msg == 0x0112) // WM_SYSCOMMAND { if (m.WParam.ToInt32() == 0xF100) // SC_KEYMENU { return; } } base.WndProc(ref m); } protected override bool ProcessDialogChar(char charCode) { // this is necessary to trap alt+char combinations so that only our hotkey system gets them return (ModifierKeys & Keys.Alt) != 0 || base.ProcessDialogChar(charCode); } private void UpdateCoreStatusBarButton() { if (Emulator.IsNull()) { CoreNameStatusBarButton.Visible = false; return; } CoreNameStatusBarButton.Visible = true; var attributes = Emulator.Attributes(); CoreNameStatusBarButton.Text = Emulator.DisplayName(); CoreNameStatusBarButton.Image = Emulator.Icon(); CoreNameStatusBarButton.ToolTipText = attributes.Ported ? "(ported) " : ""; if (Emulator.SystemId == "ZXSpectrum") { var core = (Emulation.Cores.Computers.SinclairSpectrum.ZXSpectrum)Emulator; CoreNameStatusBarButton.ToolTipText = core.GetMachineType(); } if (Emulator.SystemId == "AmstradCPC") { var core = (Emulation.Cores.Computers.AmstradCPC.AmstradCPC)Emulator; CoreNameStatusBarButton.ToolTipText = core.GetMachineType(); } } private void ToggleKeyPriority() { Config.InputHotkeyOverrideOptions++; if (Config.InputHotkeyOverrideOptions > 2) { Config.InputHotkeyOverrideOptions = 0; } UpdateKeyPriorityIcon(); switch (Config.InputHotkeyOverrideOptions) { case 0: AddOnScreenMessage("Key priority set to Both Hotkey and Input"); break; case 1: AddOnScreenMessage("Key priority set to Input over Hotkey"); break; case 2: AddOnScreenMessage("Key priority set to Input"); break; } } #endregion #region Frame Loop private void StepRunLoop_Throttle() { SyncThrottle(); _throttle.signal_frameAdvance = _runloopFrameAdvance; _throttle.signal_continuousFrameAdvancing = _runloopFrameProgress; _throttle.Step(true, -1); } public void FrameAdvance() { PressFrameAdvance = true; StepRunLoop_Core(true); } public void SeekFrameAdvance() { PressFrameAdvance = true; StepRunLoop_Core(true); PressFrameAdvance = false; } public bool IsLagFrame { get { if (Emulator.CanPollInput()) { return Emulator.AsInputPollable().IsLagFrame; } return false; } } private void StepRunLoop_Core(bool force = false) { var runFrame = false; _runloopFrameAdvance = false; var currentTimestamp = Stopwatch.GetTimestamp(); double frameAdvanceTimestampDeltaMs = (double)(currentTimestamp - _frameAdvanceTimestamp) / Stopwatch.Frequency * 1000.0; bool frameProgressTimeElapsed = frameAdvanceTimestampDeltaMs >= Config.FrameProgressDelayMs; if (Config.SkipLagFrame && IsLagFrame && frameProgressTimeElapsed && Emulator.Frame > 0) { runFrame = true; } if (ClientControls["Frame Advance"] || PressFrameAdvance || HoldFrameAdvance) { _runloopFrameAdvance = true; // handle the initial trigger of a frame advance if (_frameAdvanceTimestamp == 0) { PauseEmulator(); runFrame = true; _frameAdvanceTimestamp = currentTimestamp; } else { // handle the timed transition from countdown to FrameProgress if (frameProgressTimeElapsed) { runFrame = true; _runloopFrameProgress = true; UnpauseEmulator(); } } } else { // handle release of frame advance: do we need to deactivate FrameProgress? if (_runloopFrameProgress) { _runloopFrameProgress = false; PauseEmulator(); } _frameAdvanceTimestamp = 0; } if (!EmulatorPaused) { runFrame = true; } bool isRewinding = Rewind(ref runFrame, currentTimestamp, out var returnToRecording); float atten = 0; if (runFrame || force) { var isFastForwarding = ClientControls["Fast Forward"] || IsTurboing || InvisibleEmulation; var isFastForwardingOrRewinding = isFastForwarding || isRewinding || _unthrottled; if (isFastForwardingOrRewinding != _lastFastForwardingOrRewinding) { InitializeFpsData(); } _lastFastForwardingOrRewinding = isFastForwardingOrRewinding; // client input-related duties GlobalWin.OSD.ClearGuiText(); CheatList.Pulse(); // zero 03-may-2014 - moved this before call to UpdateToolsBefore(), since it seems to clear the state which a lua event.framestart is going to want to alter ClickyVirtualPadController.FrameTick(); Global.ButtonOverrideAdaptor.FrameTick(); if (Tools.Has() && !SuppressLua) { Tools.LuaConsole.LuaImp.CallFrameBeforeEvent(); } if (IsTurboing) { Tools.FastUpdateBefore(); } else { Tools.UpdateToolsBefore(); } if (!InvisibleEmulation) { CaptureRewind(isRewinding); } // Set volume, if enabled if (Config.SoundEnabledNormal && !InvisibleEmulation) { atten = Config.SoundVolume / 100.0f; if (isFastForwardingOrRewinding) { if (Config.SoundEnabledRWFF) { atten *= Config.SoundVolumeRWFF / 100.0f; } else { atten = 0; } } // Mute if using Frame Advance/Frame Progress if (_runloopFrameAdvance && Config.MuteFrameAdvance) { atten = 0; } } MovieSession.HandleMovieOnFrameLoop(); if (Config.AutosaveSaveRAM) { if (AutoFlushSaveRamIn-- <= 0) { FlushSaveRAM(true); } } // why not skip audio if the user doesn't want sound bool renderSound = (Config.SoundEnabled && !IsTurboing) || (_currAviWriter?.UsesAudio ?? false); if (!renderSound) { atten = 0; } bool render = !InvisibleEmulation && (!_throttle.skipNextFrame || (_currAviWriter?.UsesVideo ?? false)); bool newFrame = Emulator.FrameAdvance(Global.ControllerOutput, render, renderSound); MovieSession.HandleMovieAfterFrameLoop(); if (returnToRecording) { MovieSession.Movie.SwitchToRecord(); } if (isRewinding && !IsRewindSlave && MovieSession.Movie.IsRecording()) { MovieSession.Movie.Truncate(Global.Emulator.Frame); } CheatList.Pulse(); if (IsLagFrame && Config.AutofireLagFrames) { AutoFireController.IncrementStarts(); } Global.AutofireStickyXORAdapter.IncrementLoops(IsLagFrame); PressFrameAdvance = false; if (Tools.Has() && !SuppressLua) { Tools.LuaConsole.LuaImp.CallFrameAfterEvent(); } if (IsTurboing) { Tools.FastUpdateAfter(SuppressLua); } else { UpdateToolsAfter(SuppressLua); } if (!PauseAvi && newFrame && !InvisibleEmulation) { AvFrameAdvance(); } if (newFrame) { _framesSinceLastFpsUpdate++; UpdateFpsDisplay(currentTimestamp, isRewinding, isFastForwarding); } if (Tools.IsLoaded() && Tools.TAStudio.LastPositionFrame == Emulator.Frame) { if (PauseOnFrame.HasValue && PauseOnFrame.Value <= Tools.TAStudio.LastPositionFrame) { TasMovieRecord record = (MovieSession.Movie as TasMovie)[Emulator.Frame]; if (!record.Lagged.HasValue && IsSeeking) { // haven't yet greenzoned the frame, hence it's after editing // then we want to pause here. taseditor fashion PauseEmulator(); } } } if (IsSeeking && Emulator.Frame == PauseOnFrame.Value) { PauseEmulator(); if (Tools.IsLoaded()) { Tools.TAStudio.StopSeeking(); } PauseOnFrame = null; } } if (ClientControls["Rewind"] || PressRewind) { UpdateToolsAfter(); } Sound.UpdateSound(atten); } private void UpdateFpsDisplay(long currentTimestamp, bool isRewinding, bool isFastForwarding) { double elapsedSeconds = (currentTimestamp - _timestampLastFpsUpdate) / (double)Stopwatch.Frequency; if (elapsedSeconds < 1.0 / _fpsUpdatesPerSecond) { return; } if (_lastFps == 0) // Initial calculation { _lastFps = (_framesSinceLastFpsUpdate - 1) / elapsedSeconds; } else { _lastFps = (_lastFps + (_framesSinceLastFpsUpdate * _fpsSmoothing)) / (1.0 + (elapsedSeconds * _fpsSmoothing)); } _framesSinceLastFpsUpdate = 0; _timestampLastFpsUpdate = currentTimestamp; var fpsString = $"{_lastFps:0} fps"; if (isRewinding) { fpsString += IsTurboing || isFastForwarding ? " <<<<" : " <<"; } else if (isFastForwarding) { fpsString += IsTurboing ? " >>>>" : " >>"; } GlobalWin.OSD.Fps = fpsString; // need to refresh window caption in this case if (Config.DispSpeedupFeatures == 0) { SetWindowText(); } } private void InitializeFpsData() { _lastFps = 0; _timestampLastFpsUpdate = Stopwatch.GetTimestamp(); _framesSinceLastFpsUpdate = 0; } #endregion #region AVI Stuff /// /// start AVI recording, unattended /// /// match the short name of an /// filename to save to private void RecordAv(string videoWriterName, string filename) { RecordAvBase(videoWriterName, filename, true); } /// /// start AV recording, asking user for filename and options /// private void RecordAv() { RecordAvBase(null, null, false); } /// /// start AV recording /// private void RecordAvBase(string videoWriterName, string filename, bool unattended) { if (_currAviWriter != null || OSTailoredCode.IsUnixHost) return; // select IVideoWriter to use IVideoWriter aw; if (string.IsNullOrEmpty(videoWriterName) && !string.IsNullOrEmpty(Config.VideoWriter)) { videoWriterName = Config.VideoWriter; } _dumpaudiosync = Config.VideoWriterAudioSync; if (unattended && !string.IsNullOrEmpty(videoWriterName)) { aw = VideoWriterInventory.GetVideoWriter(videoWriterName); } else { aw = VideoWriterChooserForm.DoVideoWriterChooserDlg( VideoWriterInventory.GetAllWriters(), this, Emulator, Config, out _avwriterResizew, out _avwriterResizeh, out _avwriterpad, ref _dumpaudiosync); } if (aw == null) { AddOnScreenMessage( unattended ? $"Couldn't start video writer \"{videoWriterName}\"" : "A/V capture canceled."); return; } try { bool usingAvi = aw is AviWriter; // SO GROSS! if (_dumpaudiosync) { aw = new VideoStretcher(aw); } else { aw = new AudioStretcher(aw); } aw.SetMovieParameters(Emulator.VsyncNumerator(), Emulator.VsyncDenominator()); if (_avwriterResizew > 0 && _avwriterResizeh > 0) { aw.SetVideoParameters(_avwriterResizew, _avwriterResizeh); } else { aw.SetVideoParameters(_currentVideoProvider.BufferWidth, _currentVideoProvider.BufferHeight); } aw.SetAudioParameters(44100, 2, 16); // select codec token // do this before save dialog because ffmpeg won't know what extension it wants until it's been configured if (unattended && !string.IsNullOrEmpty(filename)) { aw.SetDefaultVideoCodecToken(); } else { // THIS IS REALLY SLOPPY! // PLEASE REDO ME TO NOT CARE WHICH AVWRITER IS USED! if (usingAvi && !string.IsNullOrEmpty(Config.AviCodecToken)) { aw.SetDefaultVideoCodecToken(); } var token = aw.AcquireVideoCodecToken(this); if (token == null) { AddOnScreenMessage("A/V capture canceled."); aw.Dispose(); return; } aw.SetVideoCodecToken(token); } // select file to save to if (unattended && !string.IsNullOrEmpty(filename)) { aw.OpenFile(filename); } else { string ext = aw.DesiredExtension(); string pathForOpenFile; // handle directories first if (ext == "") { using var fbd = new FolderBrowserEx(); if (fbd.ShowDialog() == DialogResult.Cancel) { aw.Dispose(); return; } pathForOpenFile = fbd.SelectedPath; } else { using var sfd = new SaveFileDialog(); if (Game != null) { sfd.FileName = $"{PathManager.FilesystemSafeName(Game)}.{ext}"; // don't use Path.ChangeExtension, it might wreck game names with dots in them sfd.InitialDirectory = PathManager.MakeAbsolutePath(Config.PathEntries.AvPathFragment, null); } else { sfd.FileName = "NULL"; sfd.InitialDirectory = PathManager.MakeAbsolutePath(Config.PathEntries.AvPathFragment, null); } sfd.Filter = string.Format("{0} (*.{0})|*.{0}|All Files|*.*", ext); var result = sfd.ShowHawkDialog(); if (result == DialogResult.Cancel) { aw.Dispose(); return; } pathForOpenFile = sfd.FileName; } aw.OpenFile(pathForOpenFile); } // commit the avi writing last, in case there were any errors earlier _currAviWriter = aw; AddOnScreenMessage("A/V capture started"); AVIStatusLabel.Image = Properties.Resources.AVI; AVIStatusLabel.ToolTipText = "A/V capture in progress"; AVIStatusLabel.Visible = true; } catch { AddOnScreenMessage("A/V capture failed!"); aw.Dispose(); throw; } if (_dumpaudiosync) { _currentSoundProvider.SetSyncMode(SyncSoundMode.Sync); } else { if (_currentSoundProvider.CanProvideAsync) { _currentSoundProvider.SetSyncMode(SyncSoundMode.Async); _aviSoundInputAsync = _currentSoundProvider; } else { _currentSoundProvider.SetSyncMode(SyncSoundMode.Sync); _aviSoundInputAsync = new SyncToAsyncProvider(_currentSoundProvider); } } _dumpProxy = new SimpleSyncSoundProvider(); RewireSound(); } private void AbortAv() { if (_currAviWriter == null) { _dumpProxy = null; RewireSound(); return; } _currAviWriter.Dispose(); _currAviWriter = null; AddOnScreenMessage("A/V capture aborted"); AVIStatusLabel.Image = Properties.Resources.Blank; AVIStatusLabel.ToolTipText = ""; AVIStatusLabel.Visible = false; _aviSoundInputAsync = null; _dumpProxy = null; // return to normal sound output RewireSound(); } private void StopAv() { if (_currAviWriter == null) { _dumpProxy = null; RewireSound(); return; } _currAviWriter.CloseFile(); _currAviWriter.Dispose(); _currAviWriter = null; AddOnScreenMessage("A/V capture stopped"); AVIStatusLabel.Image = Properties.Resources.Blank; AVIStatusLabel.ToolTipText = ""; AVIStatusLabel.Visible = false; _aviSoundInputAsync = null; _dumpProxy = null; // return to normal sound output RewireSound(); } private void AvFrameAdvance() { if (_currAviWriter != null) { // TODO ZERO - this code is pretty jacked. we'll want to frugalize buffers better for speedier dumping, and we might want to rely on the GL layer for padding try { // is this the best time to handle this? or deeper inside? if (_argParser._currAviWriterFrameList != null) { if (!_argParser._currAviWriterFrameList.Contains(Emulator.Frame)) { goto HANDLE_AUTODUMP; } } IVideoProvider output; IDisposable disposableOutput = null; if (_avwriterResizew > 0 && _avwriterResizeh > 0) { BitmapBuffer bbIn = null; Bitmap bmpIn = null; try { bbIn = Config.AviCaptureOsd ? CaptureOSD() : new BitmapBuffer(_currentVideoProvider.BufferWidth, _currentVideoProvider.BufferHeight, _currentVideoProvider.GetVideoBuffer()); bbIn.DiscardAlpha(); var bmpOut = new Bitmap(_avwriterResizew, _avwriterResizeh, PixelFormat.Format32bppArgb); bmpIn = bbIn.ToSysdrawingBitmap(); using (var g = Graphics.FromImage(bmpOut)) { if (_avwriterpad) { g.Clear(Color.FromArgb(_currentVideoProvider.BackgroundColor)); g.DrawImageUnscaled(bmpIn, (bmpOut.Width - bmpIn.Width) / 2, (bmpOut.Height - bmpIn.Height) / 2); } else { g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.Half; g.DrawImage(bmpIn, new Rectangle(0, 0, bmpOut.Width, bmpOut.Height)); } } output = new BmpVideoProvider(bmpOut, _currentVideoProvider.VsyncNumerator, _currentVideoProvider.VsyncDenominator); disposableOutput = (IDisposable)output; } finally { bbIn?.Dispose(); bmpIn?.Dispose(); } } else { if (Config.AviCaptureOsd) { output = new BitmapBufferVideoProvider(CaptureOSD()); disposableOutput = (IDisposable)output; } else { output = _currentVideoProvider; } } _currAviWriter.SetFrame(Emulator.Frame); short[] samp; int nsamp; if (_dumpaudiosync) { ((VideoStretcher)_currAviWriter).DumpAV(output, _currentSoundProvider, out samp, out nsamp); } else { ((AudioStretcher)_currAviWriter).DumpAV(output, _aviSoundInputAsync, out samp, out nsamp); } disposableOutput?.Dispose(); _dumpProxy.PutSamples(samp, nsamp); } catch (Exception e) { MessageBox.Show($"Video dumping died:\n\n{e}"); AbortAv(); } HANDLE_AUTODUMP: if (_argParser._autoDumpLength > 0) { _argParser._autoDumpLength--; if (_argParser._autoDumpLength == 0) // finish { StopAv(); if (_argParser._autoCloseOnDump) { _exitRequestPending = true; } } } } } private int? LoadArchiveChooser(HawkFile file) { using var ac = new ArchiveChooser(file); if (ac.ShowDialog(this) == DialogResult.OK) { return ac.SelectedMemberIndex; } return null; } #endregion #region Scheduled for refactor private void ShowMessageCoreComm(string message) { MessageBox.Show(this, message, "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); } private void ShowLoadError(object sender, RomLoader.RomErrorArgs e) { if (e.Type == RomLoader.LoadErrorType.MissingFirmware) { var result = MessageBox.Show( "You are missing the needed firmware files to load this Rom\n\nWould you like to open the firmware manager now and configure your firmwares?", e.Message, MessageBoxButtons.YesNo, MessageBoxIcon.Error); if (result == DialogResult.Yes) { FirmwaresMenuItem_Click(null, e); if (e.Retry) { // Retry loading the ROM here. This leads to recursion, as the original call to LoadRom has not exited yet, // but unless the user tries and fails to set his firmware a lot of times, nothing should happen. // Refer to how RomLoader implemented its LoadRom method for a potential fix on this. LoadRom(e.RomPath, _currentLoadRomArgs); } } } else { string title = "load error"; if (e.AttemptedCoreLoad != null) { title = $"{e.AttemptedCoreLoad} load error"; } MessageBox.Show(this, e.Message, title, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void NotifyCoreComm(string message) { AddOnScreenMessage(message); } private string ChoosePlatformForRom(RomGame rom) { using var platformChooser = new PlatformChooser(Config) { RomGame = rom }; platformChooser.ShowDialog(); return platformChooser.PlatformChoice; } public class LoadRomArgs { public bool? Deterministic { get; set; } public IOpenAdvanced OpenAdvanced { get; set; } } private LoadRomArgs _currentLoadRomArgs; public bool LoadRom(string path, LoadRomArgs args) { if (!LoadRomInternal(path, args)) return false; // what's the meaning of the last rom path when opening an archive? based on the archive file location if (args.OpenAdvanced is OpenAdvanced_OpenRom) { var leftPart = path.Split('|')[0]; Config.LastRomPath = Path.GetFullPath(Path.GetDirectoryName(leftPart) ?? ""); } return true; } // Still needs a good bit of refactoring private bool LoadRomInternal(string path, LoadRomArgs args) { if (path == null) throw new ArgumentNullException(nameof(path)); if (args == null) throw new ArgumentNullException(nameof(args)); path = EmuHawkUtil.ResolveShortcut(path); // if this is the first call to LoadRom (they will come in recursively) then stash the args bool firstCall = false; if (_currentLoadRomArgs == null) { firstCall = true; _currentLoadRomArgs = args; } else { args = _currentLoadRomArgs; } try { // movies should require deterministic emulation in ALL cases // if the core is managing its own DE through SyncSettings a 'deterministic' bool can be passed into the core's constructor // it is then up to the core itself to override its own local DeterministicEmulation setting bool deterministic = args.Deterministic ?? MovieSession.QueuedMovie != null; if (!Tools.AskSave()) { return false; } var loader = new RomLoader { ChooseArchive = LoadArchiveChooser, ChoosePlatform = ChoosePlatformForRom, Deterministic = deterministic, MessageCallback = GlobalWin.OSD.AddMessage, OpenAdvanced = args.OpenAdvanced }; Global.FirmwareManager.RecentlyServed.Clear(); loader.OnLoadError += ShowLoadError; loader.OnLoadSettings += CoreSettings; loader.OnLoadSyncSettings += CoreSyncSettings; // this also happens in CloseGame(). But it needs to happen here since if we're restarting with the same core, // any settings changes that we made need to make it back to config before we try to instantiate that core with // the new settings objects CommitCoreSettingsToConfig(); // adelikat: I Think by reordering things, this isn't necessary anymore CloseGame(); var nextComm = CreateCoreComm(); IOpenAdvanced ioa = args.OpenAdvanced; var oaOpenrom = ioa as OpenAdvanced_OpenRom; var oaMame = ioa as OpenAdvanced_MAME; var oaRetro = ioa as OpenAdvanced_Libretro; var ioaRetro = ioa as IOpenAdvancedLibretro; // we need to inform LoadRom which Libretro core to use... if (ioaRetro != null) { // prepare a core specification // if it wasn't already specified, use the current default if (ioaRetro.CorePath == null) { ioaRetro.CorePath = Config.LibretroCore; } nextComm.LaunchLibretroCore = ioaRetro.CorePath; if (nextComm.LaunchLibretroCore == null) { throw new InvalidOperationException("Can't load a file via Libretro until a core is specified"); } } if (oaOpenrom != null) { // path already has the right value, while ioa.Path is null (interestingly, these are swapped below) // I doubt null is meant to be assigned here, and it just prevents game load //path = ioa_openrom.Path; } CoreFileProvider.SyncCoreCommInputSignals(nextComm); var result = loader.LoadRom(path, nextComm); // we need to replace the path in the OpenAdvanced with the canonical one the user chose. // It can't be done until loader.LoadRom happens (for CanonicalFullPath) // i'm not sure this needs to be more abstractly engineered yet until we have more OpenAdvanced examples if (oaRetro != null) { oaRetro.token.Path = loader.CanonicalFullPath; } if (oaOpenrom != null) { oaOpenrom.Path = loader.CanonicalFullPath; } if (oaMame != null) { oaMame.Path = loader.CanonicalFullPath; } if (result) { string openAdvancedArgs = $"*{OpenAdvancedSerializer.Serialize(ioa)}"; Emulator = loader.LoadedEmulator; Global.Game = loader.Game; CoreFileProvider.SyncCoreCommInputSignals(nextComm); InputManager.SyncControls(); if (oaOpenrom != null && Path.GetExtension(oaOpenrom.Path.Replace("|", "")).ToLowerInvariant() == ".xml" && !(Emulator is LibsnesCore)) { // this is a multi-disk bundler file // determine the xml assets and create RomStatusDetails for all of them var xmlGame = XmlGame.Create(new HawkFile(oaOpenrom.Path)); using var xSw = new StringWriter(); for (int xg = 0; xg < xmlGame.Assets.Count; xg++) { var ext = Path.GetExtension(xmlGame.AssetFullPaths[xg])?.ToLowerInvariant(); if (ext == ".cue" || ext == ".ccd" || ext == ".toc" || ext == ".mds") { xSw.WriteLine(Path.GetFileNameWithoutExtension(xmlGame.Assets[xg].Key)); xSw.WriteLine("SHA1:N/A"); xSw.WriteLine("MD5:N/A"); xSw.WriteLine(); } else { xSw.WriteLine(xmlGame.Assets[xg].Key); xSw.WriteLine($"SHA1:{xmlGame.Assets[xg].Value.HashSHA1()}"); xSw.WriteLine($"MD5:{xmlGame.Assets[xg].Value.HashMD5()}"); xSw.WriteLine(); } } Emulator.CoreComm.RomStatusDetails = xSw.ToString(); Emulator.CoreComm.RomStatusAnnotation = "Multi-disk bundler"; } if (loader.LoadedEmulator is NES nes) { if (!string.IsNullOrWhiteSpace(nes.GameName)) { Game.Name = nes.GameName; } Game.Status = nes.RomStatus; } else if (loader.LoadedEmulator is QuickNES qns) { if (!string.IsNullOrWhiteSpace(qns.BootGodName)) { Game.Name = qns.BootGodName; } if (qns.BootGodStatus.HasValue) { Game.Status = qns.BootGodStatus.Value; } } if (Emulator.CoreComm.RomStatusDetails == null && loader.Rom != null) { Emulator.CoreComm.RomStatusDetails = $"{loader.Game.Name}\r\nSHA1:{loader.Rom.RomData.HashSHA1()}\r\nMD5:{loader.Rom.RomData.HashMD5()}\r\n"; } else if (Emulator.CoreComm.RomStatusDetails == null && loader.Rom == null) { // single disc game Emulator.CoreComm.RomStatusDetails = $"{loader.Game.Name}\r\nSHA1:N/A\r\nMD5:N/A\r\n"; } if (Emulator.HasBoardInfo()) { Console.WriteLine("Core reported BoardID: \"{0}\"", Emulator.AsBoardInfo().BoardName); } // restarts the lua console if a different rom is loaded. // im not really a fan of how this is done.. if (Config.RecentRoms.Empty || Config.RecentRoms.MostRecent != openAdvancedArgs) { Tools.Restart(); } Config.RecentRoms.Add(openAdvancedArgs); JumpLists.AddRecentItem(openAdvancedArgs, ioa.DisplayName); // Don't load Save Ram if a movie is being loaded if (!MovieSession.MovieIsQueued) { if (File.Exists(PathManager.SaveRamPath(loader.Game))) { LoadSaveRam(); } else if (Config.AutosaveSaveRAM && File.Exists(PathManager.AutoSaveRamPath(loader.Game))) { AddOnScreenMessage("AutoSaveRAM found, but SaveRAM was not saved"); } } Tools.Restart(Emulator); if (Config.LoadCheatFileByGame) { CheatList.SetDefaultFileName(Tools.GenerateDefaultCheatFilename()); if (CheatList.AttemptToLoadCheatFile()) { AddOnScreenMessage("Cheats file loaded"); } else if (CheatList.Any()) { CheatList.Clear(); } } CurrentlyOpenRom = oaOpenrom?.Path ?? openAdvancedArgs; CurrentlyOpenRomArgs = args; OnRomChanged(); DisplayManager.Blank(); Rewinder.Initialize(); Global.StickyXORAdapter.ClearStickies(); Global.StickyXORAdapter.ClearStickyFloats(); Global.AutofireStickyXORAdapter.ClearStickies(); RewireSound(); Tools.UpdateCheatRelatedTools(null, null); if (Config.AutoLoadLastSaveSlot && _stateSlots.HasSlot(Config.SaveSlot)) { LoadQuickSave($"QuickSave{Config.SaveSlot}"); } if (FirmwareManager.RecentlyServed.Count > 0) { Console.WriteLine("Active Firmwares:"); foreach (var f in FirmwareManager.RecentlyServed) { Console.WriteLine(" {0} : {1}", f.FirmwareId, f.Hash); } } ClientApi.OnRomLoaded(Emulator); return true; } else if (Emulator.IsNull()) { // This shows up if there's a problem ClientApi.UpdateEmulatorAndVP(Emulator); OnRomChanged(); return false; } else { // The ROM has been loaded by a recursive invocation of the LoadROM method. ClientApi.OnRomLoaded(Emulator); return true; } } finally { if (firstCall) { _currentLoadRomArgs = null; } } } private void OnRomChanged() { SetWindowText(); HandlePlatformMenus(); _stateSlots.ClearRedoList(); UpdateStatusSlots(); UpdateCoreStatusBarButton(); UpdateDumpIcon(); SetMainformMovieInfo(); } private void CommitCoreSettingsToConfig() { // save settings object var t = Emulator.GetType(); var settable = new SettingsAdapter(Emulator); if (settable.HasSettings) { Config.PutCoreSettings(settable.GetSettings(), t); } if (settable.HasSyncSettings && !MovieSession.Movie.IsActive()) { // don't trample config with loaded-from-movie settings Config.PutCoreSyncSettings(settable.GetSyncSettings(), t); } } // whats the difference between these two methods?? // its very tricky. rename to be more clear or combine them. // This gets called whenever a core related thing is changed. // Like reboot core. private void CloseGame(bool clearSram = false) { GameIsClosing = true; if (clearSram) { var path = PathManager.SaveRamPath(Game); if (File.Exists(path)) { File.Delete(path); AddOnScreenMessage("SRAM cleared."); } } else if (Emulator.HasSaveRam() && Emulator.AsSaveRam().SaveRamModified) { if (!FlushSaveRAM()) { var msgRes = MessageBox.Show("Failed flushing the game's Save RAM to your disk.\nClose without flushing Save RAM?", "Directory IO Error", MessageBoxButtons.YesNo, MessageBoxIcon.Error); if (msgRes != DialogResult.Yes) { return; } } } StopAv(); CommitCoreSettingsToConfig(); if (MovieSession.Movie.IsActive()) // Note: this must be called after CommitCoreSettingsToConfig() { StopMovie(); } Rewinder.Uninitialize(); if (Tools.IsLoaded()) { Tools.Get().Restart(); } CheatList.SaveOnClose(); Emulator.Dispose(); var coreComm = CreateCoreComm(); CoreFileProvider.SyncCoreCommInputSignals(coreComm); Emulator = new NullEmulator(coreComm); ClientApi.UpdateEmulatorAndVP(Emulator); Global.ActiveController = new Controller(NullController.Instance.Definition); Global.AutoFireController = _autofireNullControls; RewireSound(); RebootStatusBarIcon.Visible = false; GameIsClosing = false; } public bool GameIsClosing { get; private set; } // Lets tools make better decisions when being called by CloseGame public void CloseRom(bool clearSram = false) { // This gets called after Close Game gets called. // Tested with NESHawk and SMB3 (U) if (Tools.AskSave()) { CloseGame(clearSram); var coreComm = CreateCoreComm(); CoreFileProvider.SyncCoreCommInputSignals(coreComm); Emulator = new NullEmulator(coreComm); Global.Game = GameInfo.NullInstance; Tools.Restart(Emulator); RewireSound(); ClearHolds(); Tools.UpdateCheatRelatedTools(null, null); PauseOnFrame = null; CurrentlyOpenRom = null; CurrentlyOpenRomArgs = null; OnRomChanged(); } } private void ProcessMovieImport(string fn, bool start) { var result = MovieImport.ImportFile(fn); if (result.Errors.Any()) { MessageBox.Show(string.Join("\n", result.Errors), "Conversion error", MessageBoxButtons.OK, MessageBoxIcon.Error); } if (result.Warnings.Any()) { AddOnScreenMessage(result.Warnings.First()); // For now, just show the first warning } AddOnScreenMessage($"{Path.GetFileName(fn)} imported as {result.Movie.Filename}"); if (start) { StartNewMovie(result.Movie, false); Config.RecentMovies.Add(result.Movie.Filename); } } public void EnableRewind(bool enabled) { Rewinder.SuspendRewind = !enabled; AddOnScreenMessage($"Rewind {(enabled ? "enabled" : "suspended")}"); } public void ClearRewindData() { Rewinder.Clear(); } #endregion #region Tool Control API // TODO: move me public IControlMainform Master { get; private set; } private bool IsSlave => Master != null; private bool IsSavestateSlave => IsSlave && Master.WantsToControlSavestates; private bool IsRewindSlave => IsSlave && Master.WantsToControlRewind; public void RelinquishControl(IControlMainform master) { Master = master; } public void TakeBackControl() { Master = null; } private int SlotToInt(string slot) { return int.Parse(slot.Substring(slot.Length - 1, 1)); } public void LoadState(string path, string userFriendlyStateName, bool fromLua = false, bool suppressOSD = false) // Move to client.common { if (!Emulator.HasSavestates()) { return; } if (IsSavestateSlave) { Master.LoadState(); return; } // If from lua, disable counting rerecords bool wasCountingRerecords = MovieSession.Movie.IsCountingRerecords; if (fromLua) { MovieSession.Movie.IsCountingRerecords = false; } if (SavestateManager.LoadStateFile(path, userFriendlyStateName)) { GlobalWin.OSD.ClearGuiText(); ClientApi.OnStateLoaded(this, userFriendlyStateName); if (Tools.Has()) { Tools.LuaConsole.LuaImp.CallLoadStateEvent(userFriendlyStateName); } SetMainformMovieInfo(); Tools.UpdateToolsBefore(fromLua); UpdateToolsAfter(fromLua); UpdateToolsLoadstate(); AutoFireController.ClearStarts(); if (!IsRewindSlave && MovieSession.Movie.IsActive()) { ClearRewindData(); } if (!suppressOSD) { AddOnScreenMessage($"Loaded state: {userFriendlyStateName}"); } } else { AddOnScreenMessage("Loadstate error!"); } MovieSession.Movie.IsCountingRerecords = wasCountingRerecords; } public void LoadQuickSave(string quickSlotName, bool fromLua = false, bool suppressOSD = false) { if (!Emulator.HasSavestates()) { return; } ClientApi.OnBeforeQuickLoad(this, quickSlotName, out var handled); if (handled) { return; } if (IsSavestateSlave) { Master.LoadQuickSave(SlotToInt(quickSlotName)); return; } var path = $"{PathManager.SaveStatePrefix(Game)}.{quickSlotName}.State"; if (!File.Exists(path)) { AddOnScreenMessage($"Unable to load {quickSlotName}.State"); return; } LoadState(path, quickSlotName, fromLua, suppressOSD); } public void SaveState(string path, string userFriendlyStateName, bool fromLua = false, bool suppressOSD = false) { if (!Emulator.HasSavestates()) { return; } if (IsSavestateSlave) { Master.SaveState(); return; } try { SavestateManager.SaveStateFile(path, userFriendlyStateName); ClientApi.OnStateSaved(this, userFriendlyStateName); if (!suppressOSD) { AddOnScreenMessage($"Saved state: {userFriendlyStateName}"); } } catch (IOException) { AddOnScreenMessage($"Unable to save state {path}"); } if (!fromLua) { UpdateStatusSlots(); } } // TODO: should backup logic be stuffed in into Client.Common.SaveStateManager? public void SaveQuickSave(string quickSlotName, bool fromLua = false, bool suppressOSD = false) { if (!Emulator.HasSavestates()) { return; } ClientApi.OnBeforeQuickSave(this, quickSlotName, out var handled); if (handled) { return; } if (IsSavestateSlave) { Master.SaveQuickSave(SlotToInt(quickSlotName)); return; } var path = $"{PathManager.SaveStatePrefix(Game)}.{quickSlotName}.State"; var file = new FileInfo(path); if (file.Directory != null && !file.Directory.Exists) { file.Directory.Create(); } // Make backup first if (Config.BackupSavestates) { Util.TryMoveBackupFile(path, $"{path}.bak"); } SaveState(path, quickSlotName, fromLua, suppressOSD); if (Tools.Has()) { Tools.LuaConsole.LuaImp.CallSaveStateEvent(quickSlotName); } } private void SaveStateAs() { if (!Emulator.HasSavestates()) { return; } // allow named state export for tastudio, since it's safe, unlike loading one // todo: make it not save laglog in that case if (Tools.IsLoaded()) { Tools.TAStudio.NamedStatePending = true; } if (IsSavestateSlave) { Master.SaveStateAs(); return; } var path = PathManager.GetSaveStatePath(Game); var file = new FileInfo(path); if (file.Directory != null && !file.Directory.Exists) { file.Directory.Create(); } using var sfd = new SaveFileDialog { AddExtension = true, DefaultExt = "State", Filter = "Save States (*.State)|*.State|All Files|*.*", InitialDirectory = path, FileName = $"{PathManager.SaveStatePrefix(Game)}.QuickSave0.State" }; var result = sfd.ShowHawkDialog(); if (result == DialogResult.OK) { SaveState(sfd.FileName, sfd.FileName); } if (Tools.IsLoaded()) { Tools.TAStudio.NamedStatePending = false; } } private void LoadStateAs() { if (!Emulator.HasSavestates()) { return; } if (IsSavestateSlave) { Master.LoadStateAs(); return; } using var ofd = new OpenFileDialog { InitialDirectory = PathManager.GetSaveStatePath(Game), Filter = "Save States (*.State)|*.State|All Files|*.*", RestoreDirectory = true }; var result = ofd.ShowHawkDialog(); if (result != DialogResult.OK) { return; } if (!File.Exists(ofd.FileName)) { return; } LoadState(ofd.FileName, Path.GetFileName(ofd.FileName)); } private void SelectSlot(int slot) { if (Emulator.HasSavestates()) { if (IsSavestateSlave) { var handled = Master.SelectSlot(slot); if (handled) { return; } } Config.SaveSlot = slot; SaveSlotSelectedMessage(); UpdateStatusSlots(); } } private void PreviousSlot() { if (Emulator.HasSavestates()) { if (IsSavestateSlave) { var handled = Master.PreviousSlot(); if (handled) { return; } } if (Config.SaveSlot == 0) { Config.SaveSlot = 9; // Wrap to end of slot list } else if (Config.SaveSlot > 9) { Config.SaveSlot = 9; // Meh, just in case } else { Config.SaveSlot--; } SaveSlotSelectedMessage(); UpdateStatusSlots(); } } private void NextSlot() { if (Emulator.HasSavestates()) { if (IsSavestateSlave) { var handled = Master.NextSlot(); if (handled) { return; } } if (Config.SaveSlot >= 9) { Config.SaveSlot = 0; // Wrap to beginning of slot list } else if (Config.SaveSlot < 0) { Config.SaveSlot = 0; // Meh, just in case } else { Config.SaveSlot++; } SaveSlotSelectedMessage(); UpdateStatusSlots(); } } private void ToggleReadOnly() { if (IsSlave && Master.WantsToControlReadOnly) { Master.ToggleReadOnly(); } else { if (MovieSession.Movie.IsActive()) { MovieSession.ReadOnly ^= true; AddOnScreenMessage(MovieSession.ReadOnly ? "Movie read-only mode" : "Movie read+write mode"); } else { AddOnScreenMessage("No movie active"); } } } private void StopMovie(bool saveChanges = true) { if (IsSlave && Master.WantsToControlStopMovie) { Master.StopMovie(!saveChanges); } else { MovieSession.StopMovie(saveChanges); SetMainformMovieInfo(); UpdateStatusSlots(); } } private void CaptureRewind(bool suppressCaptureRewind) { if (IsRewindSlave) { Master.CaptureRewind(); } else if (!suppressCaptureRewind && Rewinder.RewindActive) { Rewinder.Capture(); } } private bool Rewind(ref bool runFrame, long currentTimestamp, out bool returnToRecording) { var isRewinding = false; returnToRecording = false; if (IsRewindSlave) { if (ClientControls["Rewind"] || PressRewind) { if (_frameRewindTimestamp == 0) { isRewinding = true; _frameRewindTimestamp = currentTimestamp; _frameRewindWasPaused = EmulatorPaused; } else { double timestampDeltaMs = (double)(currentTimestamp - _frameRewindTimestamp) / Stopwatch.Frequency * 1000.0; isRewinding = timestampDeltaMs >= Config.FrameProgressDelayMs; // clear this flag once we get out of the progress stage if (isRewinding) { _frameRewindWasPaused = false; } // if we're freely running, there's no need for reverse frame progress semantics (that may be debatable though) if (!EmulatorPaused) { isRewinding = true; } if (_frameRewindWasPaused) { if (IsSeeking) { isRewinding = false; } } } if (isRewinding) { runFrame = Emulator.Frame > 1; // TODO: the master should be deciding this! Master.Rewind(); } } else { _frameRewindTimestamp = 0; } return isRewinding; } if (Rewinder.RewindActive && (ClientControls["Rewind"] || PressRewind)) { if (EmulatorPaused) { if (_frameRewindTimestamp == 0) { isRewinding = true; _frameRewindTimestamp = currentTimestamp; } else { double timestampDeltaMs = (double)(currentTimestamp - _frameRewindTimestamp) / Stopwatch.Frequency * 1000.0; isRewinding = timestampDeltaMs >= Config.FrameProgressDelayMs; } } else { isRewinding = true; } if (isRewinding) { runFrame = Rewinder.Rewind(1) && Emulator.Frame > 1; if (runFrame && MovieSession.Movie.IsRecording()) { MovieSession.Movie.SwitchToPlay(); returnToRecording = true; } } } else { _frameRewindTimestamp = 0; } return isRewinding; } #endregion } }