From e008adf3a7f1145d2aa5eaea6a99a027f2da81a8 Mon Sep 17 00:00:00 2001 From: Sour Date: Wed, 19 May 2021 22:54:23 -0400 Subject: [PATCH] Game selection screen + load/save state screens --- Core/NES/APU/NesApu.cpp | 1 - Core/NES/NesConsole.cpp | 11 +- Core/Shared/EmuSettings.cpp | 6 +- Core/Shared/Emulator.cpp | 5 + Core/Shared/SaveStateManager.cpp | 41 ++-- Core/Shared/SaveStateManager.h | 3 +- Core/Shared/ShortcutKeyHandler.cpp | 211 +++++++++++------- Core/Shared/ShortcutKeyHandler.h | 5 + NewUI/Controls/KeyBindingButton.axaml.cs | 3 +- NewUI/Controls/MultiKeyBindingButton.axaml.cs | 3 +- NewUI/Controls/StateGrid.axaml | 40 ++++ NewUI/Controls/StateGrid.axaml.cs | 185 +++++++++++++++ NewUI/Controls/StateGridEntry.axaml | 53 +++++ NewUI/Controls/StateGridEntry.axaml.cs | 131 +++++++++++ NewUI/Interop/EmuApi.cs | 8 +- NewUI/Localization/resources.en.xml | 2 + NewUI/NewUI.csproj | 6 + NewUI/Utilities/WindowExtensions.cs | 13 +- ...nWindowModel.cs => MainWindowViewModel.cs} | 7 +- NewUI/ViewModels/RecentGamesViewModel.cs | 106 +++++++++ NewUI/Views/AudioPlayerView.axaml | 12 +- NewUI/Windows/GetKeyWindow.axaml | 2 +- NewUI/Windows/GetKeyWindow.axaml.cs | 4 +- NewUI/Windows/MainWindow.axaml | 19 +- NewUI/Windows/MainWindow.axaml.cs | 56 ++++- 25 files changed, 792 insertions(+), 141 deletions(-) create mode 100644 NewUI/Controls/StateGrid.axaml create mode 100644 NewUI/Controls/StateGrid.axaml.cs create mode 100644 NewUI/Controls/StateGridEntry.axaml create mode 100644 NewUI/Controls/StateGridEntry.axaml.cs rename NewUI/ViewModels/{MainWindowModel.cs => MainWindowViewModel.cs} (92%) create mode 100644 NewUI/ViewModels/RecentGamesViewModel.cs diff --git a/Core/NES/APU/NesApu.cpp b/Core/NES/APU/NesApu.cpp index b8ad3975..f34858f4 100644 --- a/Core/NES/APU/NesApu.cpp +++ b/Core/NES/APU/NesApu.cpp @@ -234,7 +234,6 @@ void NesApu::Serialize(Serializer& s) s.Stream(_triangleChannel.get()); s.Stream(_noiseChannel.get()); s.Stream(_deltaModulationChannel.get()); - s.Stream(_mixer); } void NesApu::AddExpansionAudioDelta(AudioChannel channel, int16_t delta) diff --git a/Core/NES/NesConsole.cpp b/Core/NES/NesConsole.cpp index 7cc734a7..a1e19d8d 100644 --- a/Core/NES/NesConsole.cpp +++ b/Core/NES/NesConsole.cpp @@ -76,13 +76,12 @@ void NesConsole::Serialize(Serializer& s) s.Stream(_apu.get()); s.Stream(_controlManager.get()); s.Stream(_mapper.get()); + s.Stream(_mixer.get()); - //TODO - /*if(_hdAudioDevice) { - _hdAudioDevice->LoadSnapshot(&loadStream, stateVersion); - } else { - Snapshotable::SkipBlock(&loadStream); - }*/ + if(_hdAudioDevice) { + //For HD packs, save the state of the bgm playback + s.Stream(_hdAudioDevice.get()); + } if(_vsSubConsole) { //For VS Dualsystem, the sub console's savestate is appended to the end of the file diff --git a/Core/Shared/EmuSettings.cpp b/Core/Shared/EmuSettings.cpp index 764816d3..ac9da322 100644 --- a/Core/Shared/EmuSettings.cpp +++ b/Core/Shared/EmuSettings.cpp @@ -20,9 +20,9 @@ EmuSettings::EmuSettings(Emulator* emu) uint32_t EmuSettings::GetVersion() { - //Version 0.4.0 - uint16_t major = 0; - uint8_t minor = 4; + //Version 2.0.0 + uint16_t major = 2; + uint8_t minor = 0; uint8_t revision = 0; return (major << 16) | (minor << 8) | revision; } diff --git a/Core/Shared/Emulator.cpp b/Core/Shared/Emulator.cpp index 5f467bba..901302d1 100644 --- a/Core/Shared/Emulator.cpp +++ b/Core/Shared/Emulator.cpp @@ -281,10 +281,12 @@ void Emulator::Stop(bool sendNotification) debugger.reset(); _videoDecoder->StopThread(); + _videoRenderer->StopThread(); _rewindManager.reset(); if(_console) { _console->Stop(); + _console.reset(); } _soundMixer->StopAudio(true); @@ -474,6 +476,9 @@ RomInfo Emulator::GetRomInfo() string Emulator::GetHash(HashType type) { //TODO + if(type == HashType::Sha1) { + return "0000000000000000000000000000000000000000"; + } return ""; } diff --git a/Core/Shared/SaveStateManager.cpp b/Core/Shared/SaveStateManager.cpp index 7ac85f9d..340a45e3 100644 --- a/Core/Shared/SaveStateManager.cpp +++ b/Core/Shared/SaveStateManager.cpp @@ -169,32 +169,28 @@ bool SaveStateManager::LoadState(istream &stream, bool hashCheckRequired) } stream.read((char*)&fileFormatVersion, sizeof(fileFormatVersion)); - if(fileFormatVersion <= 5) { + if(fileFormatVersion < SaveStateManager::MinimumSupportedVersion) { MessageManager::DisplayMessage("SaveStates", "SaveStateIncompatibleVersion"); return false; } else { char hash[41] = {}; stream.read(hash, 40); - if(fileFormatVersion >= 8) { - ConsoleType consoleType; - stream.read((char*)&consoleType, sizeof(consoleType)); - if(consoleType != _emu->GetConsoleType()) { - MessageManager::DisplayMessage("SaveStates", "SaveStateWrongSystem"); - return false; - } - } - - if(fileFormatVersion >= 7) { - #ifndef LIBRETRO - vector frameData; - uint32_t width = 0; - uint32_t height = 0; - if(GetScreenshotData(frameData, width, height, stream)) { - _emu->GetVideoDecoder()->UpdateFrame((uint16_t*)frameData.data(), width, height, 0, true, true); - } - #endif + ConsoleType consoleType; + stream.read((char*)&consoleType, sizeof(consoleType)); + if(consoleType != _emu->GetConsoleType()) { + MessageManager::DisplayMessage("SaveStates", "SaveStateWrongSystem"); + return false; } + + #ifndef LIBRETRO + vector frameData; + uint32_t width = 0; + uint32_t height = 0; + if(GetScreenshotData(frameData, width, height, stream)) { + _emu->GetVideoDecoder()->UpdateFrame((uint16_t*)frameData.data(), width, height, 0, true, true); + } + #endif uint32_t nameLength = 0; stream.read((char*)&nameLength, sizeof(uint32_t)); @@ -295,17 +291,16 @@ void SaveStateManager::LoadRecentGame(string filename, bool resetGame) std::getline(romInfoStream, romPath); std::getline(romInfoStream, patchPath); - _emu->Lock(); try { if(_emu->LoadRom(romPath, patchPath)) { if(!resetGame) { + auto lock = _emu->AcquireLock(); SaveStateManager::LoadState(stateStream, false); } } } catch(std::exception&) { _emu->Stop(true); } - _emu->Unlock(); } int32_t SaveStateManager::GetSaveStatePreview(string saveStatePath, uint8_t* pngData) @@ -328,12 +323,12 @@ int32_t SaveStateManager::GetSaveStatePreview(string saveStatePath, uint8_t* png uint32_t fileFormatVersion = 0; stream.read((char*)&fileFormatVersion, sizeof(fileFormatVersion)); - if(fileFormatVersion <= 6) { + if(fileFormatVersion < SaveStateManager::MinimumSupportedVersion) { return -1; } //Skip some header fields - stream.seekg(40, ios::cur); + stream.seekg(44, ios::cur); vector frameData; uint32_t width = 0; diff --git a/Core/Shared/SaveStateManager.h b/Core/Shared/SaveStateManager.h index cbda86ab..aa9f6ada 100644 --- a/Core/Shared/SaveStateManager.h +++ b/Core/Shared/SaveStateManager.h @@ -16,7 +16,8 @@ private: bool GetScreenshotData(vector& out, uint32_t& width, uint32_t& height, istream& stream); public: - static constexpr uint32_t FileFormatVersion = 8; + static constexpr uint32_t FileFormatVersion = 1; + static constexpr uint32_t MinimumSupportedVersion = 1; SaveStateManager(Emulator* emu); diff --git a/Core/Shared/ShortcutKeyHandler.cpp b/Core/Shared/ShortcutKeyHandler.cpp index 82c2f8b8..cc47bcba 100644 --- a/Core/Shared/ShortcutKeyHandler.cpp +++ b/Core/Shared/ShortcutKeyHandler.cpp @@ -100,95 +100,148 @@ void ShortcutKeyHandler::ProcessRunSingleFrame() _emu->PauseOnNextFrame(); } +bool ShortcutKeyHandler::IsShortcutAllowed(EmulatorShortcut shortcut) +{ + bool isRunning = _emu->IsRunning(); + bool isNetplayClient = GameClient::Connected(); + bool isMoviePlaying = _emu->GetMovieManager()->Playing(); + bool isMovieRecording = _emu->GetMovieManager()->Recording(); + bool isMovieActive = isMoviePlaying || isMovieRecording; + + switch(shortcut) { + case EmulatorShortcut::ToggleRewind: + case EmulatorShortcut::Rewind: + case EmulatorShortcut::RewindTenSecs: + case EmulatorShortcut::RewindOneMin: + return isRunning && !isNetplayClient && !isMovieRecording; + + case EmulatorShortcut::IncreaseSpeed: + case EmulatorShortcut::DecreaseSpeed: + case EmulatorShortcut::MaxSpeed: + return !isNetplayClient; + + case EmulatorShortcut::Reset: + case EmulatorShortcut::PowerCycle: + case EmulatorShortcut::ReloadRom: + return isRunning && !isNetplayClient && !isMoviePlaying; + + case EmulatorShortcut::PowerOff: + return isRunning && !isNetplayClient; + + case EmulatorShortcut::TakeScreenshot: + return isRunning; + + case EmulatorShortcut::ToggleCheats: + return !isNetplayClient && !isMovieActive; + } + + return true; +} + +void ShortcutKeyHandler::ProcessShortcutPressed(EmulatorShortcut shortcut) +{ + EmuSettings* settings = _emu->GetSettings(); + + switch(shortcut) { + case EmulatorShortcut::Pause: + if(_emu->IsPaused()) { + _emu->Resume(); + } else { + _emu->Pause(); + } + break; + + case EmulatorShortcut::Reset: _emu->Reset(); break; + case EmulatorShortcut::PowerCycle: _emu->PowerCycle(); break; + case EmulatorShortcut::ReloadRom: _emu->ReloadRom(false); break; + case EmulatorShortcut::PowerOff: _emu->Stop(true); break; + + case EmulatorShortcut::FastForward: settings->SetFlag(EmulationFlags::Turbo); break; + case EmulatorShortcut::ToggleFastForward: + if(settings->CheckFlag(EmulationFlags::Turbo)) { + settings->ClearFlag(EmulationFlags::Turbo); + } else { + settings->SetFlag(EmulationFlags::Turbo); + } + break; + + case EmulatorShortcut::SelectSaveSlot1: case EmulatorShortcut::SelectSaveSlot2: case EmulatorShortcut::SelectSaveSlot3: case EmulatorShortcut::SelectSaveSlot4: case EmulatorShortcut::SelectSaveSlot5: + case EmulatorShortcut::SelectSaveSlot6: case EmulatorShortcut::SelectSaveSlot7: case EmulatorShortcut::SelectSaveSlot8: case EmulatorShortcut::SelectSaveSlot9: case EmulatorShortcut::SelectSaveSlot10: + _emu->GetSaveStateManager()->SelectSaveSlot((int)shortcut - (int)EmulatorShortcut::SelectSaveSlot1 + 1); + break; + + case EmulatorShortcut::SaveStateSlot1: case EmulatorShortcut::SaveStateSlot2: case EmulatorShortcut::SaveStateSlot3: case EmulatorShortcut::SaveStateSlot4: case EmulatorShortcut::SaveStateSlot5: + case EmulatorShortcut::SaveStateSlot6: case EmulatorShortcut::SaveStateSlot7: case EmulatorShortcut::SaveStateSlot8: case EmulatorShortcut::SaveStateSlot9: case EmulatorShortcut::SaveStateSlot10: + _emu->GetSaveStateManager()->SaveState((int)shortcut - (int)EmulatorShortcut::SaveStateSlot1 + 1); + break; + + case EmulatorShortcut::LoadStateSlot1: case EmulatorShortcut::LoadStateSlot2: case EmulatorShortcut::LoadStateSlot3: case EmulatorShortcut::LoadStateSlot4: case EmulatorShortcut::LoadStateSlot5: + case EmulatorShortcut::LoadStateSlot6: case EmulatorShortcut::LoadStateSlot7: case EmulatorShortcut::LoadStateSlot8: case EmulatorShortcut::LoadStateSlot9: case EmulatorShortcut::LoadStateSlot10: + _emu->GetSaveStateManager()->LoadState((int)shortcut - (int)EmulatorShortcut::LoadStateSlot1 + 1); + break; + + case EmulatorShortcut::MoveToNextStateSlot: _emu->GetSaveStateManager()->MoveToNextSlot(); break; + case EmulatorShortcut::MoveToPreviousStateSlot: _emu->GetSaveStateManager()->MoveToPreviousSlot(); break; + case EmulatorShortcut::SaveState: _emu->GetSaveStateManager()->SaveState(); break; + case EmulatorShortcut::LoadState: _emu->GetSaveStateManager()->LoadState(); break; + + case EmulatorShortcut::RunSingleFrame: ProcessRunSingleFrame(); break; + + case EmulatorShortcut::ToggleRewind: + if(_emu->GetRewindManager()->IsRewinding()) { + _emu->GetRewindManager()->StopRewinding(); + } else { + _emu->GetRewindManager()->StartRewinding(); + } + break; + + case EmulatorShortcut::Rewind: _emu->GetRewindManager()->StartRewinding(); break; + case EmulatorShortcut::RewindTenSecs: _emu->GetRewindManager()->RewindSeconds(10); break; + case EmulatorShortcut::RewindOneMin: _emu->GetRewindManager()->RewindSeconds(60); break; + + default: + //Anything else is managed by the UI + break; + } +} + +void ShortcutKeyHandler::ProcessShortcutReleased(EmulatorShortcut shortcut) +{ + EmuSettings* settings = _emu->GetSettings(); + switch(shortcut) { + case EmulatorShortcut::FastForward: settings->ClearFlag(EmulationFlags::Turbo); break; + case EmulatorShortcut::Rewind: _emu->GetRewindManager()->StopRewinding(); break; + + case EmulatorShortcut::RunSingleFrame: + _runSingleFrameRepeatTimer.reset(); + _repeatStarted = false; + break; + } +} + void ShortcutKeyHandler::CheckMappedKeys() { EmuSettings* settings = _emu->GetSettings(); - bool isNetplayClient = GameClient::Connected(); - bool isMovieActive = _emu->GetMovieManager()->Playing() || _emu->GetMovieManager()->Recording(); - bool isMovieRecording = _emu->GetMovieManager()->Recording(); //Let the UI handle these shortcuts - for(uint64_t i = (uint64_t)EmulatorShortcut::TakeScreenshot; i < (uint64_t)EmulatorShortcut::ShortcutCount; i++) { - if(DetectKeyPress((EmulatorShortcut)i)) { + for(uint64_t i = 0; i < (uint64_t)EmulatorShortcut::ShortcutCount; i++) { + EmulatorShortcut shortcut = (EmulatorShortcut)i; + if(DetectKeyPress(shortcut)) { + if(!IsShortcutAllowed(shortcut)) { + continue; + } + ExecuteShortcutParams params = {}; - params.Shortcut = (EmulatorShortcut)i; + params.Shortcut = shortcut; _emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::ExecuteShortcut, ¶ms); - } else if(DetectKeyRelease((EmulatorShortcut)i)) { + + ProcessShortcutPressed(shortcut); + } else if(DetectKeyRelease(shortcut)) { ExecuteShortcutParams params = {}; - params.Shortcut = (EmulatorShortcut)i; + params.Shortcut = shortcut; _emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::ReleaseShortcut, ¶ms); - } - } - if(DetectKeyPress(EmulatorShortcut::FastForward)) { - settings->SetFlag(EmulationFlags::Turbo); - } else if(DetectKeyRelease(EmulatorShortcut::FastForward)) { - settings->ClearFlag(EmulationFlags::Turbo); - } - - if(DetectKeyPress(EmulatorShortcut::ToggleFastForward)) { - if(settings->CheckFlag(EmulationFlags::Turbo)) { - settings->ClearFlag(EmulationFlags::Turbo); - } else { - settings->SetFlag(EmulationFlags::Turbo); - } - } - - for(int i = 0; i < 10; i++) { - if(DetectKeyPress((EmulatorShortcut)((int)EmulatorShortcut::SelectSaveSlot1 + i))) { - _emu->GetSaveStateManager()->SelectSaveSlot(i + 1); - } - } - - if(DetectKeyPress(EmulatorShortcut::MoveToNextStateSlot)) { - _emu->GetSaveStateManager()->MoveToNextSlot(); - } - - if(DetectKeyPress(EmulatorShortcut::MoveToPreviousStateSlot)) { - _emu->GetSaveStateManager()->MoveToPreviousSlot(); - } - - if(DetectKeyPress(EmulatorShortcut::SaveState)) { - _emu->GetSaveStateManager()->SaveState(); - } - - if(DetectKeyPress(EmulatorShortcut::LoadState)) { - _emu->GetSaveStateManager()->LoadState(); - } - - if(DetectKeyPress(EmulatorShortcut::ToggleCheats) && !isNetplayClient && !isMovieActive) { - _emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::ExecuteShortcut, (void*)EmulatorShortcut::ToggleCheats); - } - - if(DetectKeyPress(EmulatorShortcut::RunSingleFrame)) { - ProcessRunSingleFrame(); - } - - if(DetectKeyRelease(EmulatorShortcut::RunSingleFrame)) { - _runSingleFrameRepeatTimer.reset(); - _repeatStarted = false; - } - - if(!isNetplayClient && !isMovieRecording) { - shared_ptr rewindManager = _emu->GetRewindManager(); - if(rewindManager) { - if(DetectKeyPress(EmulatorShortcut::ToggleRewind)) { - if(rewindManager->IsRewinding()) { - rewindManager->StopRewinding(); - } else { - rewindManager->StartRewinding(); - } - } - - if(DetectKeyPress(EmulatorShortcut::Rewind)) { - rewindManager->StartRewinding(); - } else if(DetectKeyRelease(EmulatorShortcut::Rewind)) { - rewindManager->StopRewinding(); - } else if(DetectKeyPress(EmulatorShortcut::RewindTenSecs)) { - rewindManager->RewindSeconds(10); - } else if(DetectKeyPress(EmulatorShortcut::RewindOneMin)) { - rewindManager->RewindSeconds(60); - } + ProcessShortcutReleased(shortcut); } } } diff --git a/Core/Shared/ShortcutKeyHandler.h b/Core/Shared/ShortcutKeyHandler.h index c1567a98..3f5fc1e5 100644 --- a/Core/Shared/ShortcutKeyHandler.h +++ b/Core/Shared/ShortcutKeyHandler.h @@ -37,6 +37,11 @@ private: void ProcessRunSingleFrame(); + bool IsShortcutAllowed(EmulatorShortcut shortcut); + + void ProcessShortcutPressed(EmulatorShortcut shortcut); + void ProcessShortcutReleased(EmulatorShortcut shortcut); + public: ShortcutKeyHandler(shared_ptr emu); ~ShortcutKeyHandler(); diff --git a/NewUI/Controls/KeyBindingButton.axaml.cs b/NewUI/Controls/KeyBindingButton.axaml.cs index 6e127c5c..c813a82c 100644 --- a/NewUI/Controls/KeyBindingButton.axaml.cs +++ b/NewUI/Controls/KeyBindingButton.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Styling; using Mesen.Interop; +using Mesen.Utilities; using Mesen.Windows; using System; @@ -42,7 +43,7 @@ namespace Mesen.Controls GetKeyWindow wnd = new GetKeyWindow(); wnd.SingleKeyMode = true; wnd.WindowStartupLocation = WindowStartupLocation.CenterOwner; - await wnd.ShowDialog(this.VisualRoot as Window); + await wnd.ShowCenteredDialog((Window)this.VisualRoot); this.KeyBinding = wnd.ShortcutKey.Key1; } diff --git a/NewUI/Controls/MultiKeyBindingButton.axaml.cs b/NewUI/Controls/MultiKeyBindingButton.axaml.cs index fc787c67..e19d35e8 100644 --- a/NewUI/Controls/MultiKeyBindingButton.axaml.cs +++ b/NewUI/Controls/MultiKeyBindingButton.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Styling; using Mesen.Config.Shortcuts; +using Mesen.Utilities; using Mesen.Windows; using System; @@ -42,7 +43,7 @@ namespace Mesen.Controls GetKeyWindow wnd = new GetKeyWindow(); wnd.SingleKeyMode = false; wnd.WindowStartupLocation = WindowStartupLocation.CenterOwner; - await wnd.ShowDialog(this.VisualRoot as Window); + await wnd.ShowCenteredDialog((Window)this.VisualRoot); this.KeyBinding = wnd.ShortcutKey; } diff --git a/NewUI/Controls/StateGrid.axaml b/NewUI/Controls/StateGrid.axaml new file mode 100644 index 00000000..1c770534 --- /dev/null +++ b/NewUI/Controls/StateGrid.axaml @@ -0,0 +1,40 @@ + + + Transparent + Transparent + + + + + + + + + + + + diff --git a/NewUI/Controls/StateGrid.axaml.cs b/NewUI/Controls/StateGrid.axaml.cs new file mode 100644 index 00000000..cf8870be --- /dev/null +++ b/NewUI/Controls/StateGrid.axaml.cs @@ -0,0 +1,185 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Mesen.ViewModels; +using System; +using Avalonia.Interactivity; +using ReactiveUI; +using System.Collections.Generic; +using Mesen.Interop; + +namespace Mesen.Controls +{ + public class StateGrid : UserControl + { + public static readonly StyledProperty> EntriesProperty = AvaloniaProperty.Register>(nameof(Entries)); + + public static readonly StyledProperty TitleProperty = AvaloniaProperty.Register(nameof(Title)); + public static readonly StyledProperty SelectedPageProperty = AvaloniaProperty.Register(nameof(SelectedPage)); + public static readonly StyledProperty ShowArrowsProperty = AvaloniaProperty.Register(nameof(ShowArrows)); + public static readonly StyledProperty ShowCloseProperty = AvaloniaProperty.Register(nameof(ShowClose)); + public static readonly StyledProperty ModeProperty = AvaloniaProperty.Register(nameof(Mode)); + + public string Title + { + get { return GetValue(TitleProperty); } + set { SetValue(TitleProperty, value); } + } + + public int SelectedPage + { + get { return GetValue(SelectedPageProperty); } + set { SetValue(SelectedPageProperty, value); } + } + + public bool ShowArrows + { + get { return GetValue(ShowArrowsProperty); } + set { SetValue(ShowArrowsProperty, value); } + } + + public bool ShowClose + { + get { return GetValue(ShowCloseProperty); } + set { SetValue(ShowCloseProperty, value); } + } + + public GameScreenMode Mode + { + get { return GetValue(ModeProperty); } + set { SetValue(ModeProperty, value); } + } + + public List Entries + { + get { return GetValue(EntriesProperty); } + set { SetValue(EntriesProperty, value); } + } + + private int _colCount = 0; + private int _rowCount = 0; + + private int ElementsPerPage => _rowCount * _colCount; + private int PageCount => (int)Math.Ceiling((double)Entries.Count / ElementsPerPage); + + static StateGrid() + { + BoundsProperty.Changed.AddClassHandler((x, e) => x.InitGrid()); + EntriesProperty.Changed.AddClassHandler((x, e) => { + x.SelectedPage = 0; + x.InitGrid(true); + }); + SelectedPageProperty.Changed.AddClassHandler((x, e) => x.InitGrid(true)); + } + + public StateGrid() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnCloseClick(object sender, RoutedEventArgs e) + { + if(DataContext is RecentGamesViewModel model) { + if(model.NeedResume) { + EmuApi.Resume(); + } + model.Visible = false; + } + } + + private void OnPrevPageClick(object sender, RoutedEventArgs e) + { + int page = SelectedPage - 1; + if(page < 0) { + page = PageCount - 1; + } + SelectedPage = page; + } + + private void OnNextPageClick(object sender, RoutedEventArgs e) + { + int page = SelectedPage + 1; + if(page >= PageCount) { + page = 0; + } + SelectedPage = page; + } + + private void InitGrid(bool forceUpdate = false) + { + Grid grid = this.FindControl("Grid"); + Size size = grid.Bounds.Size; + + int colCount = Math.Min(4, Math.Max(1, (int)(size.Width / 300))); + int rowCount = Math.Min(3, Math.Max(1, (int)(size.Height / 300))); + + if(Entries.Count <= 1) { + colCount = 1; + rowCount = 1; + } else if(Entries.Count <= 4) { + colCount = Math.Min(2, colCount); + rowCount = colCount; + } + + if(Mode != GameScreenMode.RecentGames) { + colCount = 4; + rowCount = 3; + } + + bool layoutChanged = _colCount != colCount || _rowCount != rowCount; + if(!forceUpdate && !layoutChanged) { + //Grid is already the same size + return; + } + + if(layoutChanged) { + SelectedPage = 0; + } + + _colCount = colCount; + _rowCount = rowCount; + + grid.Children.Clear(); + + grid.ColumnDefinitions = new ColumnDefinitions(); + for(int i = 0; i < colCount; i++) { + grid.ColumnDefinitions.Add(new ColumnDefinition(1, GridUnitType.Star)); + } + + grid.RowDefinitions = new RowDefinitions(); + for(int i = 0; i < rowCount; i++) { + grid.RowDefinitions.Add(new RowDefinition(1, GridUnitType.Star)); + } + + int elementsPerPage = ElementsPerPage; + int startIndex = elementsPerPage * SelectedPage; + + ShowArrows = Entries.Count > elementsPerPage; + ShowClose = Mode != GameScreenMode.RecentGames; + + for(int row = 0; row < rowCount; row++) { + for(int col = 0; col < colCount; col++) { + int index = startIndex + row * colCount + col; + + if(index >= Entries.Count) { + break; + } + + StateGridEntry ctrl = new StateGridEntry(); + + ctrl.SetValue(Grid.ColumnProperty, col); + ctrl.SetValue(Grid.RowProperty, row); + ctrl.Entry = Entries[index]; + ctrl.Init(); + + grid.Children.Add(ctrl); + } + } + } + } +} diff --git a/NewUI/Controls/StateGridEntry.axaml b/NewUI/Controls/StateGridEntry.axaml new file mode 100644 index 00000000..29c62298 --- /dev/null +++ b/NewUI/Controls/StateGridEntry.axaml @@ -0,0 +1,53 @@ + + + Gray + + + + + + + + + + + + \ No newline at end of file diff --git a/NewUI/Controls/StateGridEntry.axaml.cs b/NewUI/Controls/StateGridEntry.axaml.cs new file mode 100644 index 00000000..2b71e2ae --- /dev/null +++ b/NewUI/Controls/StateGridEntry.axaml.cs @@ -0,0 +1,131 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Mesen.Interop; +using Mesen.Localization; +using Mesen.ViewModels; +using ReactiveUI.Fody.Helpers; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; + +namespace Mesen.Controls +{ + public class StateGridEntry : UserControl + { + private static readonly WriteableBitmap EmptyImage = new WriteableBitmap(new PixelSize(256, 240), new Vector(96, 96), Avalonia.Platform.PixelFormat.Rgba8888, Avalonia.Platform.AlphaFormat.Opaque); + + public static readonly StyledProperty EntryProperty = AvaloniaProperty.Register(nameof(Entry)); + public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register(nameof(Image)); + public static readonly StyledProperty TitleProperty = AvaloniaProperty.Register(nameof(Title)); + public static readonly StyledProperty SubTitleProperty = AvaloniaProperty.Register(nameof(SubTitle)); + public static readonly StyledProperty EnabledProperty = AvaloniaProperty.Register(nameof(Enabled)); + + public RecentGameInfo Entry + { + get { return GetValue(EntryProperty); } + set { SetValue(EntryProperty, value); } + } + + public Bitmap? Image + { + get { return GetValue(ImageProperty); } + set { SetValue(ImageProperty, value); } + } + + public bool Enabled + { + get { return GetValue(EnabledProperty); } + set { SetValue(EnabledProperty, value); } + } + + public string Title + { + get { return GetValue(TitleProperty); } + set { SetValue(TitleProperty, value); } + } + + public string SubTitle + { + get { return GetValue(SubTitleProperty); } + set { SetValue(SubTitleProperty, value); } + } + + public StateGridEntry() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void OnImageClick(object sender, RoutedEventArgs e) + { + RecentGameInfo game = Entry; + if(Path.GetExtension(game.FileName) == ".mss") { + if(game.SaveMode) { + EmuApi.SaveStateFile(game.FileName); + } else { + EmuApi.LoadStateFile(game.FileName); + } + EmuApi.Resume(); + } else { + EmuApi.LoadRecentGame(Entry.FileName, false); + } + } + + public void Init() + { + RecentGameInfo game = Entry; + if(game == null) { + return; + } + + Title = game.Name; + + bool fileExists = File.Exists(game.FileName); + if(fileExists) { + SubTitle = new FileInfo(game.FileName).LastWriteTime.ToString(); + } else { + SubTitle = ResourceHelper.GetMessage("EmptyState"); + } + Enabled = fileExists || game.SaveMode; + + if(fileExists) { + Task.Run(() => { + Bitmap? img = null; + try { + if(Path.GetExtension(game.FileName) == ".mss") { + img = EmuApi.GetSaveStatePreview(game.FileName); + } else { + using FileStream fs = File.Open(game.FileName, FileMode.Open, FileAccess.Read, FileShare.Read); + ZipArchive zip = new ZipArchive(fs); + ZipArchiveEntry? entry = zip.GetEntry("Screenshot.png"); + if(entry != null) { + using Stream stream = entry.Open(); + + //Copy to a memory stream (to avoid what looks like a Skia or Avalonia issue?) + using MemoryStream ms = new MemoryStream(); + stream.CopyTo(ms); + ms.Seek(0, SeekOrigin.Begin); + + img = new Bitmap(ms); + } + } + } catch { } + + Dispatcher.UIThread.Post(() => { + Image = img; + }); + }); + } else { + Image = StateGridEntry.EmptyImage; + } + } + } +} diff --git a/NewUI/Interop/EmuApi.cs b/NewUI/Interop/EmuApi.cs index 60d2e453..ad32ccff 100644 --- a/NewUI/Interop/EmuApi.cs +++ b/NewUI/Interop/EmuApi.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Mesen.Localization; using Mesen.Config.Shortcuts; using Mesen.Utilities; +using Avalonia.Media.Imaging; namespace Mesen.Interop { @@ -94,8 +95,7 @@ namespace Mesen.Interop [DllImport(DllPath)] public static extern void LoadStateFile([MarshalAs(UnmanagedType.LPUTF8Str)]string filepath); [DllImport(DllPath, EntryPoint = "GetSaveStatePreview")] private static extern Int32 GetSaveStatePreviewWrapper([MarshalAs(UnmanagedType.LPUTF8Str)]string saveStatePath, [Out]byte[] imgData); - //TODO - /*public static Image GetSaveStatePreview(string saveStatePath) + public static Bitmap? GetSaveStatePreview(string saveStatePath) { if(File.Exists(saveStatePath)) { byte[] buffer = new byte[512*478*4]; @@ -103,12 +103,12 @@ namespace Mesen.Interop if(size > 0) { Array.Resize(ref buffer, size); using(MemoryStream stream = new MemoryStream(buffer)) { - return Image.FromStream(stream); + return new Bitmap(stream); } } } return null; - }*/ + } [DllImport(DllPath)] public static extern void SetCheats([In]UInt32[] cheats, UInt32 cheatCount); [DllImport(DllPath)] public static extern void ClearCheats(); diff --git a/NewUI/Localization/resources.en.xml b/NewUI/Localization/resources.en.xml index 672bd642..d1c89f1e 100644 --- a/NewUI/Localization/resources.en.xml +++ b/NewUI/Localization/resources.en.xml @@ -9,6 +9,8 @@ Save State Load State Load Last Session + Load State Menu + Save State Menu Recent Files Exit Game diff --git a/NewUI/NewUI.csproj b/NewUI/NewUI.csproj index eee8dad3..fc9cb8a8 100644 --- a/NewUI/NewUI.csproj +++ b/NewUI/NewUI.csproj @@ -199,6 +199,9 @@ GameboyConfigView.axaml + + StateGridEntry.axaml + SnesConfigView.axaml @@ -214,6 +217,9 @@ NesControllerView.axaml + + StateGrid.axaml + ColorPickerWindow.axaml diff --git a/NewUI/Utilities/WindowExtensions.cs b/NewUI/Utilities/WindowExtensions.cs index e51a18a1..d8370e78 100644 --- a/NewUI/Utilities/WindowExtensions.cs +++ b/NewUI/Utilities/WindowExtensions.cs @@ -10,13 +10,24 @@ namespace Mesen.Utilities { static class WindowExtensions { - public static void ShowCentered(this Window child, Window parent) + private static void CenterWindow(Window child, Window parent) { child.WindowStartupLocation = WindowStartupLocation.Manual; Size wndCenter = (parent.ClientSize / 2); PixelPoint screenCenter = new PixelPoint(parent.Position.X + (int)wndCenter.Width, parent.Position.Y + (int)wndCenter.Height); child.Position = new PixelPoint(screenCenter.X - (int)child.Width / 2, screenCenter.Y - (int)child.Height / 2); + } + + public static void ShowCentered(this Window child, Window parent) + { + CenterWindow(child, parent); child.Show(); } + + public static Task ShowCenteredDialog(this Window child, Window parent) + { + CenterWindow(child, parent); + return child.ShowDialog(parent); + } } } diff --git a/NewUI/ViewModels/MainWindowModel.cs b/NewUI/ViewModels/MainWindowViewModel.cs similarity index 92% rename from NewUI/ViewModels/MainWindowModel.cs rename to NewUI/ViewModels/MainWindowViewModel.cs index 90fe1903..b6df7bb7 100644 --- a/NewUI/ViewModels/MainWindowModel.cs +++ b/NewUI/ViewModels/MainWindowViewModel.cs @@ -27,17 +27,22 @@ namespace Mesen.ViewModels [Reactive] public bool HasRecentItems { get; private set; } public ReactiveCommand OpenRecentCommand { get; } + [Reactive] public RecentGamesViewModel RecentGames { get; private set; } + public MainWindowViewModel() { OpenRecentCommand = ReactiveCommand.Create(OpenRecent); RomInfo = new RomInfo(); + RecentItems = ConfigManager.Config.RecentFiles.Items; - this.WhenAnyValue(x => x.RecentItems.Count).Subscribe(count => { HasRecentItems = count > 0; }); + RecentGames = new RecentGamesViewModel(); + RecentGames.Init(GameScreenMode.RecentGames); + this.WhenAnyValue(x => x.RomInfo).Subscribe(x => { IsGameRunning = x.Format != RomFormat.Unknown; diff --git a/NewUI/ViewModels/RecentGamesViewModel.cs b/NewUI/ViewModels/RecentGamesViewModel.cs new file mode 100644 index 00000000..62fd2da3 --- /dev/null +++ b/NewUI/ViewModels/RecentGamesViewModel.cs @@ -0,0 +1,106 @@ +using Mesen.Config; +using Mesen.Interop; +using Mesen.Localization; +using Mesen.Utilities; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Mesen.ViewModels +{ + public class RecentGamesViewModel : ViewModelBase + { + [Reactive] public bool Visible { get; set; } + [Reactive] public bool NeedResume { get; private set; } + [Reactive] public string Title { get; private set; } = ""; + [Reactive] public GameScreenMode Mode { get; private set; } + [Reactive] public List GameEntries { get; private set; } = new List(); + + public RecentGamesViewModel() + { + } + + public void Init(GameScreenMode mode) + { + if(mode == GameScreenMode.RecentGames && ConfigManager.Config.Preferences.DisableGameSelectionScreen) { + Visible = false; + GameEntries = new List(); + return; + } else if(mode != GameScreenMode.RecentGames && Mode == mode && Visible) { + Visible = false; + if(NeedResume) { + EmuApi.Resume(); + } + return; + } + + Mode = mode; + + List entries = new(); + + if(mode == GameScreenMode.RecentGames) { + NeedResume = false; + Title = string.Empty; + + List files = Directory.GetFiles(ConfigManager.RecentGamesFolder, "*.rgd").OrderByDescending((file) => new FileInfo(file).LastWriteTime).ToList(); + for(int i = 0; i < files.Count && entries.Count < 72; i++) { + entries.Add(new RecentGameInfo() { FileName = files[i], Name = Path.GetFileNameWithoutExtension(files[i]) }); + } + } else { + if(!Visible) { + NeedResume = Pause(); + } + + Title = mode == GameScreenMode.LoadState ? ResourceHelper.GetMessage("LoadStateDialog") : ResourceHelper.GetMessage("SaveStateDialog"); + + string romName = EmuApi.GetRomInfo().GetRomName(); + for(int i = 0; i < (mode == GameScreenMode.LoadState ? 11 : 10); i++) { + entries.Add(new RecentGameInfo() { + FileName = Path.Combine(ConfigManager.SaveStateFolder, romName + "_" + (i + 1) + ".mss"), + Name = i == 10 ? ResourceHelper.GetMessage("AutoSave") : ResourceHelper.GetMessage("SlotNumber", i + 1), + SaveMode = mode == GameScreenMode.SaveState + }); + } + if(mode == GameScreenMode.LoadState) { + entries.Add(new RecentGameInfo() { + FileName = Path.Combine(ConfigManager.RecentGamesFolder, romName + ".rgd"), + Name = ResourceHelper.GetMessage("LastSession") + }); + } + } + + Visible = entries.Count > 0; + GameEntries = entries; + } + + private bool Pause() + { + if(!EmuApi.IsPaused()) { + EmuApi.Pause(); + return true; + } + return false; + } + } + + public enum GameScreenMode + { + RecentGames, + LoadState, + SaveState + } + + public class RecentGameInfo + { + public string FileName { get; set; } + public string Name { get; set; } + public bool SaveMode { get; set; } + } +} diff --git a/NewUI/Views/AudioPlayerView.axaml b/NewUI/Views/AudioPlayerView.axaml index 1b3e1416..b0683be6 100644 --- a/NewUI/Views/AudioPlayerView.axaml +++ b/NewUI/Views/AudioPlayerView.axaml @@ -27,17 +27,25 @@ + + + - -