Game selection screen + load/save state screens

This commit is contained in:
Sour 2021-05-19 22:54:23 -04:00
parent bb692e90c1
commit e008adf3a7
25 changed files with 792 additions and 141 deletions

View file

@ -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)

View file

@ -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

View file

@ -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;
}

View file

@ -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 "";
}

View file

@ -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<uint8_t> 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<uint8_t> 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<uint8_t> frameData;
uint32_t width = 0;

View file

@ -16,7 +16,8 @@ private:
bool GetScreenshotData(vector<uint8_t>& 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);

View file

@ -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, &params);
} 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, &params);
}
}
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> 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);
}
}
}

View file

@ -37,6 +37,11 @@ private:
void ProcessRunSingleFrame();
bool IsShortcutAllowed(EmulatorShortcut shortcut);
void ProcessShortcutPressed(EmulatorShortcut shortcut);
void ProcessShortcutReleased(EmulatorShortcut shortcut);
public:
ShortcutKeyHandler(shared_ptr<Emulator> emu);
~ShortcutKeyHandler();

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,40 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Mesen.ViewModels"
xmlns:c="using:Mesen.Controls"
xmlns:cfg="using:Mesen.Config"
xmlns:l="using:Mesen.Localization"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="400"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Name="root"
x:Class="Mesen.Controls.StateGrid"
>
<UserControl.Resources>
<Color x:Key="ButtonBackgroundPointerOver">Transparent</Color>
<Color x:Key="ButtonBackgroundPressed">Transparent</Color>
</UserControl.Resources>
<DockPanel Margin="5" DataContext="{Binding ElementName=root}">
<Panel DockPanel.Dock="Top">
<TextBlock Text="{Binding Title}" FontSize="15" FontWeight="Medium" Foreground="White" TextAlignment="Center" />
<Button IsVisible="{Binding ShowClose}" Click="OnCloseClick" HorizontalAlignment="Right" Background="Transparent" BorderThickness="0" Padding="0" Margin="0 -2 0 -2">
<Image Source="/Assets/CloseWhite.png" Width="16" Height="16" />
</Button>
</Panel>
<Button DockPanel.Dock="Left" IsVisible="{Binding ShowArrows}" Background="Transparent" BorderThickness="0" Click="OnPrevPageClick" VerticalAlignment="Stretch" Height="NaN">
<Image Source="/Assets/MediaPlay.png" VerticalAlignment="Center" HorizontalAlignment="Left" Width="16">
<Image.RenderTransform>
<RotateTransform Angle="180" />
</Image.RenderTransform>
</Image>
</Button>
<Button DockPanel.Dock="Right" IsVisible="{Binding ShowArrows}" Background="Transparent" BorderThickness="0" Click="OnNextPageClick" VerticalAlignment="Stretch" Height="NaN">
<Image Source="/Assets/MediaPlay.png" VerticalAlignment="Center" HorizontalAlignment="Right" Width="16" />
</Button>
<Grid Name="Grid" />
</DockPanel>
</UserControl>

View file

@ -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<List<RecentGameInfo>> EntriesProperty = AvaloniaProperty.Register<StateGrid, List<RecentGameInfo>>(nameof(Entries));
public static readonly StyledProperty<string> TitleProperty = AvaloniaProperty.Register<StateGrid, string>(nameof(Title));
public static readonly StyledProperty<int> SelectedPageProperty = AvaloniaProperty.Register<StateGrid, int>(nameof(SelectedPage));
public static readonly StyledProperty<bool> ShowArrowsProperty = AvaloniaProperty.Register<StateGrid, bool>(nameof(ShowArrows));
public static readonly StyledProperty<bool> ShowCloseProperty = AvaloniaProperty.Register<StateGrid, bool>(nameof(ShowClose));
public static readonly StyledProperty<GameScreenMode> ModeProperty = AvaloniaProperty.Register<StateGrid, GameScreenMode>(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<RecentGameInfo> 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<StateGrid>((x, e) => x.InitGrid());
EntriesProperty.Changed.AddClassHandler<StateGrid>((x, e) => {
x.SelectedPage = 0;
x.InitGrid(true);
});
SelectedPageProperty.Changed.AddClassHandler<StateGrid>((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>("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);
}
}
}
}
}

View file

@ -0,0 +1,53 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="100" d:DesignHeight="100"
xmlns:c="using:Mesen.Controls"
x:Name="root"
HorizontalAlignment="Stretch"
x:Class="Mesen.Controls.StateGridEntry"
>
<UserControl.Resources>
<Color x:Key="ButtonBorderBrushDisabled">Gray</Color>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="TextBlock:disabled">
<Setter Property="Foreground" Value="Gray" />
</Style>
</UserControl.Styles>
<DockPanel Margin="2 2 2 7" DataContext="{Binding ElementName=root}">
<TextBlock
DockPanel.Dock="Bottom"
Text="{Binding SubTitle}"
TextAlignment="Center"
TextWrapping="Wrap"
IsEnabled="{Binding Enabled}"
/>
<TextBlock
DockPanel.Dock="Bottom"
Text="{Binding Title}"
TextAlignment="Center"
TextWrapping="Wrap"
IsEnabled="{Binding Enabled}"
/>
<Button
BorderBrush="SlateGray"
BorderThickness="2"
Height="NaN"
Background="Transparent"
Click="OnImageClick"
IsEnabled="{Binding Enabled}"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Padding="0"
>
<Image Source="{Binding Image}" Stretch="Uniform" StretchDirection="Both" VerticalAlignment="Center" />
</Button>
</DockPanel>
</UserControl>

View file

@ -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<RecentGameInfo> EntryProperty = AvaloniaProperty.Register<StateGridEntry, RecentGameInfo>(nameof(Entry));
public static readonly StyledProperty<Bitmap?> ImageProperty = AvaloniaProperty.Register<StateGridEntry, Bitmap?>(nameof(Image));
public static readonly StyledProperty<string> TitleProperty = AvaloniaProperty.Register<StateGridEntry, string>(nameof(Title));
public static readonly StyledProperty<string> SubTitleProperty = AvaloniaProperty.Register<StateGridEntry, string>(nameof(SubTitle));
public static readonly StyledProperty<bool> EnabledProperty = AvaloniaProperty.Register<StateGridEntry, bool>(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;
}
}
}
}

View file

@ -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();

View file

@ -9,6 +9,8 @@
<Control ID="mnuSaveState">Save State</Control>
<Control ID="mnuLoadState">Load State</Control>
<Control ID="mnuLoadLastSession">Load Last Session</Control>
<Control ID="mnuLoadStateMenu">Load State Menu</Control>
<Control ID="mnuSaveStateMenu">Save State Menu</Control>
<Control ID="mnuRecentFiles">Recent Files</Control>
<Control ID="mnuExit">Exit</Control>
<Control ID="mnuGame">Game</Control>

View file

@ -199,6 +199,9 @@
<Compile Update="Views\GameboyConfigView.axaml.cs">
<DependentUpon>GameboyConfigView.axaml</DependentUpon>
</Compile>
<Compile Update="Controls\StateGridEntry.axaml.cs">
<DependentUpon>StateGridEntry.axaml</DependentUpon>
</Compile>
<Compile Update="Views\SnesConfigView.axaml.cs">
<DependentUpon>SnesConfigView.axaml</DependentUpon>
</Compile>
@ -214,6 +217,9 @@
<Compile Update="Views\NesControllerView.axaml.cs">
<DependentUpon>NesControllerView.axaml</DependentUpon>
</Compile>
<Compile Update="Controls\StateGrid.axaml.cs">
<DependentUpon>StateGrid.axaml</DependentUpon>
</Compile>
<Compile Update="Windows\ColorPickerWindow.axaml.cs">
<DependentUpon>ColorPickerWindow.axaml</DependentUpon>
</Compile>

View file

@ -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);
}
}
}

View file

@ -27,17 +27,22 @@ namespace Mesen.ViewModels
[Reactive] public bool HasRecentItems { get; private set; }
public ReactiveCommand<RecentItem, Unit> OpenRecentCommand { get; }
[Reactive] public RecentGamesViewModel RecentGames { get; private set; }
public MainWindowViewModel()
{
OpenRecentCommand = ReactiveCommand.Create<RecentItem>(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;

View file

@ -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<RecentGameInfo> GameEntries { get; private set; } = new List<RecentGameInfo>();
public RecentGamesViewModel()
{
}
public void Init(GameScreenMode mode)
{
if(mode == GameScreenMode.RecentGames && ConfigManager.Config.Preferences.DisableGameSelectionScreen) {
Visible = false;
GameEntries = new List<RecentGameInfo>();
return;
} else if(mode != GameScreenMode.RecentGames && Mode == mode && Visible) {
Visible = false;
if(NeedResume) {
EmuApi.Resume();
}
return;
}
Mode = mode;
List<RecentGameInfo> entries = new();
if(mode == GameScreenMode.RecentGames) {
NeedResume = false;
Title = string.Empty;
List<string> 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; }
}
}

View file

@ -27,17 +27,25 @@
<Style Selector="Slider TickBar">
<Setter Property="Fill" Value="Gray" />
</Style>
<Style Selector="Button.Toggle">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="Button.Toggle:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
</UserControl.Styles>
<Panel Background="Black">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Left">
<Button Click="OnToggleShuffleClick" Height="26">
<Button Click="OnToggleShuffleClick" Height="26" Classes="Toggle">
<Panel>
<Image Height="24" Width="24" Source="/Assets/ShuffleEnabled.png" IsVisible="{CompiledBinding Config.Shuffle}" />
<Image Height="24" Width="24" Source="/Assets/Shuffle.png" IsVisible="{CompiledBinding !Config.Shuffle}" />
</Panel>
</Button>
<Button Click="OnToggleRepeatClick" Height="26">
<Button Click="OnToggleRepeatClick" Height="26" Classes="Toggle">
<Panel>
<Image Height="24" Width="24" Source="/Assets/RepeatEnabled.png" IsVisible="{CompiledBinding Config.Repeat}" />
<Image Height="24" Width="24" Source="/Assets/Repeat.png" IsVisible="{CompiledBinding !Config.Repeat}" />

View file

@ -25,7 +25,7 @@
TextWrapping="Wrap"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
/>
</Panel>
<TextBlock
Grid.Row="1"

View file

@ -34,14 +34,14 @@ namespace Mesen.Windows
AvaloniaXamlLoader.Load(this);
}
protected void OnPreviewKeyDown(object? sender, KeyEventArgs e)
private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
{
InputApi.SetKeyState((int)e.Key, true);
this.OnKeyChange();
e.Handled = true;
}
protected void OnPreviewKeyUp(object? sender, KeyEventArgs e)
private void OnPreviewKeyUp(object? sender, KeyEventArgs e)
{
InputApi.SetKeyState((int)e.Key, false);
this.OnKeyChange();

View file

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:m="clr-namespace:Mesen"
xmlns:c="using:Mesen.Controls"
xmlns:v="using:Mesen.Views"
xmlns:l="using:Mesen.Localization"
xmlns:vm="using:Mesen.ViewModels"
@ -23,6 +24,9 @@
<DataTemplate DataType="{x:Type vm:AudioPlayerViewModel}">
<v:AudioPlayerView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:RecentGamesViewModel}">
<c:StateGrid Entries="{Binding GameEntries}" Title="{Binding Title}" Mode="{Binding Mode}" />
</DataTemplate>
</Window.DataTemplates>
<DockPanel Background="Black">
@ -32,11 +36,11 @@
<MenuItem.Icon><Image Source="/Assets/Folder.png" /></MenuItem.Icon>
</MenuItem>
<Separator/>
<MenuItem Header="{l:Translate mnuSaveState}" Click="OnExitClick">
<MenuItem Header="{l:Translate mnuSaveState}">
<MenuItem Header="{l:Translate mnuSaveStateMenu}" Click="OnSaveStateMenuClick" IsEnabled="{CompiledBinding IsGameRunning}" />
</MenuItem>
<MenuItem Header="{l:Translate mnuLoadState}" Click="OnExitClick">
<MenuItem Header="{l:Translate mnuLoadState}">
<MenuItem Header="{l:Translate mnuLoadStateMenu}" Click="OnLoadStateMenuClick" IsEnabled="{CompiledBinding IsGameRunning}" />
</MenuItem>
<Separator/>
<MenuItem Header="{l:Translate mnuRecentFiles}" Items="{CompiledBinding RecentItems}" Classes="RecentFiles" IsEnabled="{CompiledBinding HasRecentItems}">
@ -237,7 +241,12 @@
</MenuItem>
</MenuItem>
</Menu>
<ContentControl DockPanel.Dock="Bottom" Content="{CompiledBinding AudioPlayer}" />
<m:NativeRenderer Name="Renderer" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Panel>
<ContentControl Content="{CompiledBinding RecentGames}" IsVisible="{CompiledBinding RecentGames.Visible}" />
<m:NativeRenderer Name="Renderer" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{CompiledBinding !RecentGames.Visible}" />
</Panel>
</DockPanel>
</Window>

View file

@ -26,14 +26,21 @@ namespace Mesen.Windows
{
public class MainWindow : Window
{
private NotificationListener? _listener;
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
private MainWindowViewModel _model = null;
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
private NotificationListener? _listener = null;
private ConfigWindow? _cfgWindow = null;
private MainWindowViewModel? _model = null;
public MainWindow()
{
InitializeComponent();
AddHandler(DragDrop.DropEvent, OnDrop);
//Allow us to catch LeftAlt/RightAlt key presses
AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel, true);
AddHandler(InputElement.KeyUpEvent, OnPreviewKeyUp, RoutingStrategies.Tunnel, true);
#if DEBUG
this.AttachDevTools();
#endif
@ -88,10 +95,10 @@ namespace Mesen.Windows
//ConfigApi.SetEmulationFlag(EmulationFlags.MaximumSpeed, true);
if(!ProcessCommandLineArgs(Program.CommandLineArgs)) {
/*if(!ProcessCommandLineArgs(Program.CommandLineArgs)) {
EmuApi.LoadRom(@"C:\Code\Games\Super Mario Bros. (USA).nes");
//EmuApi.LoadRom(@"C:\Code\Mesen-S\PGOHelper\PGOGames\Super Mario World (USA).sfc");
}
}*/
ConfigManager.Config.Preferences.UpdateFileAssociations();
@ -120,13 +127,21 @@ namespace Mesen.Windows
case ConsoleNotificationType.GameLoaded:
RomInfo romInfo = EmuApi.GetRomInfo();
Dispatcher.UIThread.Post(() => {
_model!.RomInfo = romInfo;
_model.RecentGames.Visible = false;
_model.RomInfo = romInfo;
});
break;
case ConsoleNotificationType.GameResumed:
Dispatcher.UIThread.Post(() => {
_model.RecentGames.Visible = false;
});
break;
case ConsoleNotificationType.EmulationStopped:
Dispatcher.UIThread.Post(() => {
_model!.RomInfo = new RomInfo();
_model.RomInfo = new RomInfo();
_model.RecentGames.Init(GameScreenMode.RecentGames);
});
break;
@ -164,6 +179,16 @@ namespace Mesen.Windows
LoadRomHelper.LoadFile(filenames[0]);
}
}
private void OnSaveStateMenuClick(object sender, RoutedEventArgs e)
{
_model.RecentGames.Init(GameScreenMode.SaveState);
}
private void OnLoadStateMenuClick(object sender, RoutedEventArgs e)
{
_model.RecentGames.Init(GameScreenMode.LoadState);
}
private void OnTileViewerClick(object sender, RoutedEventArgs e)
{
@ -201,6 +226,11 @@ namespace Mesen.Windows
private void cfgWindow_Closed(object? sender, EventArgs e)
{
_cfgWindow = null;
if(ConfigManager.Config.Preferences.DisableGameSelectionScreen && _model.RecentGames.Visible) {
_model.RecentGames.Visible = false;
} else if(!ConfigManager.Config.Preferences.DisableGameSelectionScreen && !_model.IsGameRunning) {
_model.RecentGames.Init(GameScreenMode.RecentGames);
}
}
private void OnPreferencesClick(object sender, RoutedEventArgs e)
@ -332,16 +362,22 @@ namespace Mesen.Windows
EmuApi.ExecuteShortcut(new ExecuteShortcutParams() { Shortcut = EmulatorShortcut.FdsInsertDiskNumber, Param = 1 });
}
protected override void OnKeyDown(KeyEventArgs e)
private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
{
base.OnKeyDown(e);
InputApi.SetKeyState((int)e.Key, true);
if(e.Key == Key.Tab) {
e.Handled = true;
}
}
protected override void OnKeyUp(KeyEventArgs e)
private void OnPreviewKeyUp(object? sender, KeyEventArgs e)
{
base.OnKeyUp(e);
InputApi.SetKeyState((int)e.Key, false);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
InputApi.ResetKeyState();
}
}
}