Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
3a4eeb77fe | |||
51b3953cfc | |||
610eecc1c1 | |||
492056abf6 | |||
ee6e682ab4 | |||
6f60e102a2 | |||
eeb2af9953 | |||
550747eac6 | |||
3ffceab1fb | |||
b9f2a96595 | |||
cbaa845f5d | |||
9e2681f2d7 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -69,6 +69,12 @@ jobs:
|
||||
- name: Publish Ryujinx.Ava
|
||||
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.DOTNET_RUNTIME_IDENTIFIER }}" -o ./publish_ava -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER Ryujinx.Ava --self-contained true
|
||||
if: github.event_name == 'pull_request'
|
||||
- name: Rename Avalonia (Windows)
|
||||
run: mv ./publish_ava/Ryujinx.Ava.exe ./publish_ava/Ryujinx.exe
|
||||
if: runner.os == 'Windows' && github.event_name == 'pull_request'
|
||||
- name: Rename Avalonia (Unix)
|
||||
run: mv ./publish_ava/Ryujinx.Ava ./publish_ava/Ryujinx
|
||||
if: runner.os != 'Windows' && github.event_name == 'pull_request'
|
||||
- name: Upload Ryujinx artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
29
.github/workflows/release.yml
vendored
29
.github/workflows/release.yml
vendored
@ -38,19 +38,21 @@ jobs:
|
||||
shell: bash
|
||||
- name: Configure for release
|
||||
run: |
|
||||
sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' Ryujinx.Common/ReleaseInformations.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' Ryujinx.Common/ReleaseInformations.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' Ryujinx.Common/ReleaseInformations.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' Ryujinx.Common/ReleaseInformations.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' Ryujinx.Common/ReleaseInformations.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
- name: Create output dir
|
||||
run: "mkdir release_output"
|
||||
|
||||
- name: Publish Windows
|
||||
run: |
|
||||
dotnet publish -c Release -r win10-x64 -o ./publish_windows/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx --self-contained true
|
||||
dotnet publish -c Release -r win10-x64 -o ./publish_windows_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx.Headless.SDL2 --self-contained true
|
||||
dotnet publish -c Release -r win10-x64 -o ./publish_windows_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx.Ava --self-contained true
|
||||
mv ./publish_windows_ava/publish/Ryujinx.Ava.exe ./publish_windows_ava/publish/Ryujinx.exe
|
||||
- name: Packing Windows builds
|
||||
run: |
|
||||
pushd publish_windows
|
||||
@ -71,19 +73,28 @@ jobs:
|
||||
dotnet publish -c Release -r linux-x64 -o ./publish_linux/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx --self-contained true
|
||||
dotnet publish -c Release -r linux-x64 -o ./publish_linux_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx.Headless.SDL2 --self-contained true
|
||||
dotnet publish -c Release -r linux-x64 -o ./publish_linux_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded Ryujinx.Ava --self-contained true
|
||||
|
||||
mv ./publish_linux_ava/publish/Ryujinx.Ava ./publish_linux_ava/publish/Ryujinx
|
||||
- name: Packing Linux builds
|
||||
run: |
|
||||
pushd publish_linux
|
||||
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz publish
|
||||
tar --exclude "publish/Ryujinx" -cvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar publish
|
||||
python3 ../distribution/misc/add_tar_exec.py ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar "publish/Ryujinx" "publish/Ryujinx"
|
||||
gzip -9 < ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar > ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz
|
||||
rm ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar
|
||||
popd
|
||||
|
||||
pushd publish_linux_sdl2_headless
|
||||
tar -czvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz publish
|
||||
tar --exclude "publish/Ryujinx.Headless.SDL2" -cvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar publish
|
||||
python3 ../distribution/misc/add_tar_exec.py ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar "publish/Ryujinx.Headless.SDL2" "publish/Ryujinx.Headless.SDL2"
|
||||
gzip -9 < ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar > ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz
|
||||
rm ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-linux_x64.tar
|
||||
popd
|
||||
|
||||
pushd publish_linux_ava
|
||||
tar -czvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz publish
|
||||
tar --exclude "publish/Ryujinx" -cvf ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar publish
|
||||
python3 ../distribution/misc/add_tar_exec.py ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar "publish/Ryujinx" "publish/Ryujinx"
|
||||
gzip -9 < ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar > ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz
|
||||
rm ../release_output/test-ava-ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
using ARMeilleure.Translation;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Threading;
|
||||
using LibHac.Tools.FsSystem;
|
||||
@ -13,6 +15,7 @@ using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
@ -42,7 +45,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||
using Key = Ryujinx.Input.Key;
|
||||
using MouseButton = Ryujinx.Input.MouseButton;
|
||||
@ -67,7 +70,7 @@ namespace Ryujinx.Ava
|
||||
private readonly AccountManager _accountManager;
|
||||
private readonly UserChannelPersistence _userChannelPersistence;
|
||||
private readonly InputManager _inputManager;
|
||||
private readonly MainWindow _parent;
|
||||
private readonly MainWindowViewModel _viewModel;
|
||||
private readonly IKeyboard _keyboardInterface;
|
||||
private readonly GraphicsDebugLevel _glLogLevel;
|
||||
|
||||
@ -110,6 +113,7 @@ namespace Ryujinx.Ava
|
||||
public bool ScreenshotRequested { get; set; }
|
||||
|
||||
private object _lockObject = new();
|
||||
private TopLevel _topLevel;
|
||||
|
||||
public AppHost(
|
||||
RendererHost renderer,
|
||||
@ -119,9 +123,10 @@ namespace Ryujinx.Ava
|
||||
ContentManager contentManager,
|
||||
AccountManager accountManager,
|
||||
UserChannelPersistence userChannelPersistence,
|
||||
MainWindow parent)
|
||||
MainWindowViewModel viewmodel,
|
||||
TopLevel topLevel)
|
||||
{
|
||||
_parent = parent;
|
||||
_viewModel = viewmodel;
|
||||
_inputManager = inputManager;
|
||||
_accountManager = accountManager;
|
||||
_userChannelPersistence = userChannelPersistence;
|
||||
@ -129,7 +134,8 @@ namespace Ryujinx.Ava
|
||||
_hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
_glLogLevel = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
|
||||
_inputManager.SetMouseDriver(new AvaloniaMouseDriver(_parent, renderer));
|
||||
_topLevel = topLevel;
|
||||
_inputManager.SetMouseDriver(new AvaloniaMouseDriver(_topLevel, renderer));
|
||||
_keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
|
||||
|
||||
NpadManager = _inputManager.CreateNpadManager();
|
||||
@ -144,15 +150,15 @@ namespace Ryujinx.Ava
|
||||
|
||||
if (ApplicationPath.StartsWith("@SystemContent"))
|
||||
{
|
||||
ApplicationPath = _parent.VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath);
|
||||
ApplicationPath = _viewModel.VirtualFileSystem.SwitchPathToSystemPath(ApplicationPath);
|
||||
|
||||
_isFirmwareTitle = true;
|
||||
}
|
||||
|
||||
ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
|
||||
|
||||
_parent.PointerLeave += Parent_PointerLeft;
|
||||
_parent.PointerMoved += Parent_PointerMoved;
|
||||
_topLevel.PointerLeave += TopLevel_PointerLeave;
|
||||
_topLevel.PointerMoved += TopLevel_PointerMoved;
|
||||
|
||||
ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
|
||||
ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
|
||||
@ -162,25 +168,27 @@ namespace Ryujinx.Ava
|
||||
_gpuCancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private void Parent_PointerMoved(object sender, PointerEventArgs e)
|
||||
private void TopLevel_PointerMoved(object sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Control visual)
|
||||
{
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
var p = e.GetCurrentPoint(_parent).Position;
|
||||
var r = _parent.InputHitTest(p);
|
||||
_isMouseInRenderer = r == Renderer;
|
||||
var point = e.GetCurrentPoint(visual).Position;
|
||||
_isMouseInRenderer = Equals(visual.InputHitTest(point), Renderer);
|
||||
}
|
||||
}
|
||||
|
||||
private void Parent_PointerLeft(object sender, PointerEventArgs e)
|
||||
private void TopLevel_PointerLeave(object sender, PointerEventArgs e)
|
||||
{
|
||||
_isMouseInRenderer = false;
|
||||
_parent.Cursor = Cursor.Default;
|
||||
_viewModel.Cursor = Cursor.Default;
|
||||
}
|
||||
|
||||
private void SetRendererWindowSize(Size size)
|
||||
{
|
||||
if (_renderer != null)
|
||||
{
|
||||
double scale = _parent.PlatformImpl.RenderScaling;
|
||||
double scale = _topLevel.PlatformImpl.RenderScaling;
|
||||
_renderer.Window?.SetSize((int)(size.Width * scale), (int)(size.Height * scale));
|
||||
}
|
||||
}
|
||||
@ -256,7 +264,7 @@ namespace Ryujinx.Ava
|
||||
NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
|
||||
TouchScreenManager.Initialize(Device);
|
||||
|
||||
_parent.ViewModel.IsGameRunning = true;
|
||||
_viewModel.IsGameRunning = true;
|
||||
|
||||
string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName)
|
||||
? string.Empty
|
||||
@ -276,10 +284,10 @@ namespace Ryujinx.Ava
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
_parent.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
|
||||
_viewModel.Title = $"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
|
||||
});
|
||||
|
||||
_parent.ViewModel.SetUiProgressHandlers(Device);
|
||||
_viewModel.SetUIProgressHandlers(Device);
|
||||
|
||||
Renderer.SizeChanged += Window_SizeChanged;
|
||||
|
||||
@ -287,7 +295,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
_renderingThread.Start();
|
||||
|
||||
_parent.ViewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
|
||||
_viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
|
||||
|
||||
MainLoop();
|
||||
|
||||
@ -321,7 +329,7 @@ namespace Ryujinx.Ava
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var value = e.NewValue;
|
||||
_parent.ViewModel.Volume = e.NewValue;
|
||||
_viewModel.Volume = e.NewValue;
|
||||
});
|
||||
}
|
||||
|
||||
@ -369,7 +377,7 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
if (Device.Application != null)
|
||||
{
|
||||
_parent.UpdateGameMetadata(Device.Application.TitleIdText);
|
||||
_viewModel.UpdateGameMetadata(Device.Application.TitleIdText);
|
||||
}
|
||||
|
||||
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
|
||||
@ -377,6 +385,9 @@ namespace Ryujinx.Ava
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState;
|
||||
ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState;
|
||||
|
||||
_topLevel.PointerLeave -= TopLevel_PointerLeave;
|
||||
_topLevel.PointerMoved -= TopLevel_PointerMoved;
|
||||
|
||||
_gpuCancellationTokenSource.Cancel();
|
||||
_gpuCancellationTokenSource.Dispose();
|
||||
|
||||
@ -410,7 +421,7 @@ namespace Ryujinx.Ava
|
||||
}
|
||||
else
|
||||
{
|
||||
_parent.Cursor = Cursor.Default;
|
||||
_viewModel.Cursor = Cursor.Default;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -422,7 +433,10 @@ namespace Ryujinx.Ava
|
||||
|
||||
SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
|
||||
|
||||
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (!SetupValidator.CanStartApplication(ContentManager, ApplicationPath, out UserError userError))
|
||||
{
|
||||
{
|
||||
if (SetupValidator.CanFixStartApplication(ContentManager, ApplicationPath, userError, out firmwareVersion))
|
||||
{
|
||||
@ -430,14 +444,15 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedMessage], firmwareVersion.VersionString),
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedMessage],
|
||||
firmwareVersion.VersionString),
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
"");
|
||||
|
||||
if (result != UserResult.Yes)
|
||||
{
|
||||
await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
|
||||
await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow));
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
@ -446,7 +461,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
if (!SetupValidator.TryFixStartApplication(ContentManager, ApplicationPath, userError, out _))
|
||||
{
|
||||
await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
|
||||
await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow));
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
@ -457,11 +472,13 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
|
||||
|
||||
_parent.RefreshFirmwareStatus();
|
||||
_viewModel.RefreshFirmwareStatus();
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstalledMessage], firmwareVersion.VersionString),
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage], firmwareVersion.VersionString),
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstalledMessage],
|
||||
firmwareVersion.VersionString),
|
||||
string.Format(LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage],
|
||||
firmwareVersion.VersionString),
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||
"",
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
||||
@ -469,12 +486,14 @@ namespace Ryujinx.Ava
|
||||
}
|
||||
else
|
||||
{
|
||||
await UserErrorDialog.ShowUserErrorDialog(userError, _parent);
|
||||
await UserErrorDialog.ShowUserErrorDialog(userError, (desktop.MainWindow as MainWindow));
|
||||
Device.Dispose();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}");
|
||||
|
||||
@ -567,7 +586,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
DiscordIntegrationModule.SwitchToPlayingState(Device.Application.TitleIdText, Device.Application.TitleName);
|
||||
|
||||
_parent.ApplicationLibrary.LoadAndSaveMetaData(Device.Application.TitleIdText, appMetadata =>
|
||||
_viewModel.ApplicationLibrary.LoadAndSaveMetaData(Device.Application.TitleIdText, appMetadata =>
|
||||
{
|
||||
appMetadata.LastPlayed = DateTime.UtcNow.ToString();
|
||||
});
|
||||
@ -578,13 +597,13 @@ namespace Ryujinx.Ava
|
||||
internal void Resume()
|
||||
{
|
||||
Device?.System.TogglePauseEmulation(false);
|
||||
_parent.ViewModel.IsPaused = false;
|
||||
_viewModel.IsPaused = false;
|
||||
}
|
||||
|
||||
internal void Pause()
|
||||
{
|
||||
Device?.System.TogglePauseEmulation(true);
|
||||
_parent.ViewModel.IsPaused = true;
|
||||
_viewModel.IsPaused = true;
|
||||
}
|
||||
|
||||
private void InitializeSwitchInstance()
|
||||
@ -632,7 +651,7 @@ namespace Ryujinx.Ava
|
||||
Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration.");
|
||||
|
||||
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl;
|
||||
MainWindow.SaveConfig();
|
||||
MainWindowViewModel.SaveConfig();
|
||||
|
||||
deviceDriver = new OpenALHardwareDeviceDriver();
|
||||
}
|
||||
@ -645,7 +664,7 @@ namespace Ryujinx.Ava
|
||||
Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration.");
|
||||
|
||||
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo;
|
||||
MainWindow.SaveConfig();
|
||||
MainWindowViewModel.SaveConfig();
|
||||
|
||||
deviceDriver = new SoundIoHardwareDeviceDriver();
|
||||
}
|
||||
@ -671,7 +690,7 @@ namespace Ryujinx.Ava
|
||||
Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration.");
|
||||
|
||||
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2;
|
||||
MainWindow.SaveConfig();
|
||||
MainWindowViewModel.SaveConfig();
|
||||
|
||||
deviceDriver = new SDL2HardwareDeviceDriver();
|
||||
}
|
||||
@ -684,7 +703,7 @@ namespace Ryujinx.Ava
|
||||
Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration.");
|
||||
|
||||
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl;
|
||||
MainWindow.SaveConfig();
|
||||
MainWindowViewModel.SaveConfig();
|
||||
|
||||
deviceDriver = new OpenALHardwareDeviceDriver();
|
||||
}
|
||||
@ -710,7 +729,7 @@ namespace Ryujinx.Ava
|
||||
Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration.");
|
||||
|
||||
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2;
|
||||
MainWindow.SaveConfig();
|
||||
MainWindowViewModel.SaveConfig();
|
||||
|
||||
deviceDriver = new SDL2HardwareDeviceDriver();
|
||||
}
|
||||
@ -723,7 +742,7 @@ namespace Ryujinx.Ava
|
||||
Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration.");
|
||||
|
||||
ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo;
|
||||
MainWindow.SaveConfig();
|
||||
MainWindowViewModel.SaveConfig();
|
||||
|
||||
deviceDriver = new SoundIoHardwareDeviceDriver();
|
||||
}
|
||||
@ -740,14 +759,14 @@ namespace Ryujinx.Ava
|
||||
IntegrityCheckLevel fsIntegrityCheckLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None;
|
||||
|
||||
HLE.HLEConfiguration configuration = new HLE.HLEConfiguration(VirtualFileSystem,
|
||||
_parent.LibHacHorizonManager,
|
||||
_viewModel.LibHacHorizonManager,
|
||||
ContentManager,
|
||||
_accountManager,
|
||||
_userChannelPersistence,
|
||||
renderer,
|
||||
deviceDriver,
|
||||
memoryConfiguration,
|
||||
_parent.UiHandler,
|
||||
_viewModel.UiHandler,
|
||||
(SystemLanguage)ConfigurationState.Instance.System.Language.Value,
|
||||
(RegionCode)ConfigurationState.Instance.System.Region.Value,
|
||||
ConfigurationState.Instance.Graphics.EnableVsync,
|
||||
@ -788,14 +807,14 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (_parent.ViewModel.StartGamesInFullscreen)
|
||||
if (_viewModel.StartGamesInFullscreen)
|
||||
{
|
||||
_parent.WindowState = WindowState.FullScreen;
|
||||
_viewModel.WindowState = WindowState.FullScreen;
|
||||
}
|
||||
|
||||
if (_parent.WindowState == WindowState.FullScreen)
|
||||
if (_viewModel.WindowState == WindowState.FullScreen)
|
||||
{
|
||||
_parent.ViewModel.ShowMenuAndStatusBar = false;
|
||||
_viewModel.ShowMenuAndStatusBar = false;
|
||||
}
|
||||
});
|
||||
|
||||
@ -819,7 +838,7 @@ namespace Ryujinx.Ava
|
||||
Width = (int)Renderer.Bounds.Width;
|
||||
Height = (int)Renderer.Bounds.Height;
|
||||
|
||||
_renderer.Window.SetSize((int)(Width * _parent.PlatformImpl.RenderScaling), (int)(Height * _parent.PlatformImpl.RenderScaling));
|
||||
_renderer.Window.SetSize((int)(Width * _topLevel.PlatformImpl.RenderScaling), (int)(Height * _topLevel.PlatformImpl.RenderScaling));
|
||||
|
||||
_chrono.Start();
|
||||
|
||||
@ -847,7 +866,7 @@ namespace Ryujinx.Ava
|
||||
if (!_renderingStarted)
|
||||
{
|
||||
_renderingStarted = true;
|
||||
_parent.SwitchToGameControl();
|
||||
_viewModel.SwitchToRenderer(false);
|
||||
}
|
||||
|
||||
Device.PresentFrame(() => Renderer?.SwapBuffers());
|
||||
@ -914,7 +933,7 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_parent.Cursor = _isMouseInRenderer ? InvisibleCursor : Cursor.Default;
|
||||
_viewModel.Cursor = _isMouseInRenderer ? InvisibleCursor : Cursor.Default;
|
||||
});
|
||||
}
|
||||
else
|
||||
@ -925,7 +944,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_parent.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default;
|
||||
_viewModel.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -938,13 +957,13 @@ namespace Ryujinx.Ava
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_parent.IsActive)
|
||||
if (_viewModel.IsActive)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
HandleScreenState();
|
||||
|
||||
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _parent.WindowState != WindowState.FullScreen)
|
||||
if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
|
||||
{
|
||||
Device.Application.DiskCacheLoadState?.Cancel();
|
||||
}
|
||||
@ -953,7 +972,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
||||
|
||||
if (_parent.IsActive)
|
||||
if (_viewModel.IsActive)
|
||||
{
|
||||
KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
|
||||
|
||||
@ -969,10 +988,10 @@ namespace Ryujinx.Ava
|
||||
ScreenshotRequested = true;
|
||||
break;
|
||||
case KeyboardHotkeyState.ShowUi:
|
||||
_parent.ViewModel.ShowMenuAndStatusBar = true;
|
||||
_viewModel.ShowMenuAndStatusBar = true;
|
||||
break;
|
||||
case KeyboardHotkeyState.Pause:
|
||||
if (_parent.ViewModel.IsPaused)
|
||||
if (_viewModel.IsPaused)
|
||||
{
|
||||
Resume();
|
||||
}
|
||||
@ -991,7 +1010,7 @@ namespace Ryujinx.Ava
|
||||
Device.SetVolume(0);
|
||||
}
|
||||
|
||||
_parent.ViewModel.Volume = Device.GetVolume();
|
||||
_viewModel.Volume = Device.GetVolume();
|
||||
break;
|
||||
case KeyboardHotkeyState.ResScaleUp:
|
||||
GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
|
||||
@ -1004,13 +1023,13 @@ namespace Ryujinx.Ava
|
||||
_newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2);
|
||||
Device.SetVolume(_newVolume);
|
||||
|
||||
_parent.ViewModel.Volume = Device.GetVolume();
|
||||
_viewModel.Volume = Device.GetVolume();
|
||||
break;
|
||||
case KeyboardHotkeyState.VolumeDown:
|
||||
_newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2);
|
||||
Device.SetVolume(_newVolume);
|
||||
|
||||
_parent.ViewModel.Volume = Device.GetVolume();
|
||||
_viewModel.Volume = Device.GetVolume();
|
||||
break;
|
||||
case KeyboardHotkeyState.None:
|
||||
(_keyboardInterface as AvaloniaKeyboard).Clear();
|
||||
@ -1030,7 +1049,7 @@ namespace Ryujinx.Ava
|
||||
// Touchscreen
|
||||
bool hasTouch = false;
|
||||
|
||||
if (_parent.IsActive && !ConfigurationState.Instance.Hid.EnableMouse)
|
||||
if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse)
|
||||
{
|
||||
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
||||
}
|
||||
|
@ -269,6 +269,8 @@
|
||||
<Color x:Key="DataGridSelectionColor">#FF00FABB</Color>
|
||||
<Color x:Key="ThemeContentBackgroundColor">#FF2D2D2D</Color>
|
||||
<Color x:Key="ThemeControlBorderColor">#FF505050</Color>
|
||||
<Color x:Key="VsyncEnabled">#FF2EEAC9</Color>
|
||||
<Color x:Key="VsyncDisabled">#FFFF4554</Color>
|
||||
<x:Double x:Key="ScrollBarThickness">15</x:Double>
|
||||
<x:Double x:Key="FontSizeSmall">8</x:Double>
|
||||
<x:Double x:Key="FontSizeNormal">10</x:Double>
|
||||
|
111
Ryujinx.Ava/Helper/LoggerAdapter.cs
Normal file
111
Ryujinx.Ava/Helper/LoggerAdapter.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using Avalonia.Utilities;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helper
|
||||
{
|
||||
using AvaLogger = Avalonia.Logging.Logger;
|
||||
using AvaLogLevel = Avalonia.Logging.LogEventLevel;
|
||||
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
||||
using RyuLogClass = Ryujinx.Common.Logging.LogClass;
|
||||
|
||||
internal class LoggerAdapter : Avalonia.Logging.ILogSink
|
||||
{
|
||||
public static void Register()
|
||||
{
|
||||
AvaLogger.Sink = new LoggerAdapter();
|
||||
}
|
||||
|
||||
private static RyuLogger.Log? GetLog(AvaLogLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
AvaLogLevel.Verbose => RyuLogger.Trace,
|
||||
AvaLogLevel.Debug => RyuLogger.Debug,
|
||||
AvaLogLevel.Information => RyuLogger.Info,
|
||||
AvaLogLevel.Warning => RyuLogger.Warning,
|
||||
AvaLogLevel.Error => RyuLogger.Error,
|
||||
AvaLogLevel.Fatal => RyuLogger.Notice,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsEnabled(AvaLogLevel level, string area)
|
||||
{
|
||||
return GetLog(level) != null;
|
||||
}
|
||||
|
||||
public void Log(AvaLogLevel level, string area, object source, string messageTemplate)
|
||||
{
|
||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, null));
|
||||
}
|
||||
|
||||
public void Log<T0>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0)
|
||||
{
|
||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0 }));
|
||||
}
|
||||
|
||||
public void Log<T0, T1>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1)
|
||||
{
|
||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0, propertyValue1 }));
|
||||
}
|
||||
|
||||
public void Log<T0, T1, T2>(AvaLogLevel level, string area, object source, string messageTemplate, T0 propertyValue0, T1 propertyValue1, T2 propertyValue2)
|
||||
{
|
||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, new object[] { propertyValue0, propertyValue1, propertyValue2 }));
|
||||
}
|
||||
|
||||
public void Log(AvaLogLevel level, string area, object source, string messageTemplate, params object[] propertyValues)
|
||||
{
|
||||
GetLog(level)?.PrintMsg(RyuLogClass.Ui, Format(area, messageTemplate, source, propertyValues));
|
||||
}
|
||||
|
||||
private static string Format(string area, string template, object source, object[] v)
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
var r = new CharacterReader(template.AsSpan());
|
||||
var i = 0;
|
||||
|
||||
result.Append('[');
|
||||
result.Append(area);
|
||||
result.Append("] ");
|
||||
|
||||
while (!r.End)
|
||||
{
|
||||
var c = r.Take();
|
||||
|
||||
if (c != '{')
|
||||
{
|
||||
result.Append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (r.Peek != '{')
|
||||
{
|
||||
result.Append('\'');
|
||||
result.Append(i < v.Length ? v[i++] : null);
|
||||
result.Append('\'');
|
||||
r.TakeUntil('}');
|
||||
r.Take();
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append('{');
|
||||
r.Take();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source != null)
|
||||
{
|
||||
result.Append(" (");
|
||||
result.Append(source.GetType().Name);
|
||||
result.Append(" #");
|
||||
result.Append(source.GetHashCode());
|
||||
result.Append(')');
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ namespace Ryujinx.Ava.Input
|
||||
private Control _widget;
|
||||
private bool _isDisposed;
|
||||
private Size _size;
|
||||
private readonly Window _window;
|
||||
private readonly TopLevel _window;
|
||||
|
||||
public bool[] PressedButtons { get; }
|
||||
public Vector2 CurrentPosition { get; private set; }
|
||||
@ -23,7 +23,7 @@ namespace Ryujinx.Ava.Input
|
||||
public string DriverName => "AvaloniaMouseDriver";
|
||||
public ReadOnlySpan<string> GamepadsIds => new[] { "0" };
|
||||
|
||||
public AvaloniaMouseDriver(Window window, Control parent)
|
||||
public AvaloniaMouseDriver(TopLevel window, Control parent)
|
||||
{
|
||||
_widget = parent;
|
||||
_window = window;
|
||||
|
@ -56,7 +56,7 @@ namespace Ryujinx.Modules
|
||||
}
|
||||
|
||||
Running = true;
|
||||
mainWindow.CanUpdate = false;
|
||||
mainWindow.ViewModel.CanUpdate = false;
|
||||
|
||||
// Detect current platform
|
||||
if (OperatingSystem.IsMacOS())
|
||||
@ -95,7 +95,7 @@ namespace Ryujinx.Modules
|
||||
{
|
||||
using (HttpClient jsonClient = ConstructHttpClient())
|
||||
{
|
||||
string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformations.ReleaseChannelOwner}/{ReleaseInformations.ReleaseChannelRepo}/releases/latest";
|
||||
string buildInfoURL = $"{GitHubApiURL}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
|
||||
|
||||
string fetchedJson = await jsonClient.GetStringAsync(buildInfoURL);
|
||||
JObject jsonRoot = JObject.Parse(fetchedJson);
|
||||
@ -182,7 +182,7 @@ namespace Ryujinx.Modules
|
||||
}
|
||||
|
||||
Running = false;
|
||||
mainWindow.CanUpdate = true;
|
||||
mainWindow.ViewModel.CanUpdate = true;
|
||||
|
||||
return;
|
||||
}
|
||||
@ -625,7 +625,7 @@ namespace Ryujinx.Modules
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Program.Version.Contains("dirty") || !ReleaseInformations.IsValid())
|
||||
if (Program.Version.Contains("dirty") || !ReleaseInformation.IsValid())
|
||||
{
|
||||
if (showWarnings)
|
||||
{
|
||||
@ -640,7 +640,7 @@ namespace Ryujinx.Modules
|
||||
#else
|
||||
if (showWarnings)
|
||||
{
|
||||
if (ReleaseInformations.IsFlatHubBuild())
|
||||
if (ReleaseInformation.IsFlatHubBuild())
|
||||
{
|
||||
ContentDialogHelper.CreateWarningDialog(LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage]);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.UI.Helper;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
@ -13,8 +14,10 @@ using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava
|
||||
@ -32,9 +35,51 @@ namespace Ryujinx.Ava
|
||||
|
||||
private const uint MB_ICONWARNING = 0x30;
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
static void RegisterMimeTypes()
|
||||
{
|
||||
if (ReleaseInformation.IsFlatHubBuild())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string mimeDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "mime");
|
||||
|
||||
if (!File.Exists(Path.Combine(mimeDbPath, "packages", "Ryujinx.xml")))
|
||||
{
|
||||
string mimeTypesFile = Path.Combine(ReleaseInformation.GetBaseApplicationDirectory(), "mime", "Ryujinx.xml");
|
||||
using Process mimeProcess = new();
|
||||
|
||||
mimeProcess.StartInfo.FileName = "xdg-mime";
|
||||
mimeProcess.StartInfo.Arguments = $"install --novendor --mode user {mimeTypesFile}";
|
||||
|
||||
mimeProcess.Start();
|
||||
mimeProcess.WaitForExit();
|
||||
|
||||
if (mimeProcess.ExitCode != 0)
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, $"Unable to install mime types. Make sure xdg-utils is installed. Process exited with code: {mimeProcess.ExitCode}");
|
||||
return;
|
||||
}
|
||||
|
||||
using Process updateMimeProcess = new();
|
||||
|
||||
updateMimeProcess.StartInfo.FileName = "update-mime-database";
|
||||
updateMimeProcess.StartInfo.Arguments = mimeDbPath;
|
||||
|
||||
updateMimeProcess.Start();
|
||||
updateMimeProcess.WaitForExit();
|
||||
|
||||
if (updateMimeProcess.ExitCode != 0)
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, $"Could not update local mime database. Process exited with code: {updateMimeProcess.ExitCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Version = ReleaseInformations.GetVersion();
|
||||
Version = ReleaseInformation.GetVersion();
|
||||
|
||||
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
|
||||
{
|
||||
@ -45,6 +90,8 @@ namespace Ryujinx.Ava
|
||||
|
||||
Initialize(args);
|
||||
|
||||
LoggerAdapter.Register();
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
|
||||
@ -66,8 +113,7 @@ namespace Ryujinx.Ava
|
||||
AllowEglInitialization = false,
|
||||
CompositionBackdropCornerRadius = 8.0f,
|
||||
})
|
||||
.UseSkia()
|
||||
.LogToTrace();
|
||||
.UseSkia();
|
||||
}
|
||||
|
||||
private static void Initialize(string[] args)
|
||||
@ -93,6 +139,12 @@ namespace Ryujinx.Ava
|
||||
// Initialize the logger system.
|
||||
LoggerModule.Initialize();
|
||||
|
||||
// Register mime types on linux.
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
RegisterMimeTypes();
|
||||
}
|
||||
|
||||
// Initialize Discord integration.
|
||||
DiscordIntegrationModule.Initialize();
|
||||
|
||||
|
@ -76,6 +76,16 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64'">
|
||||
<Content Include="..\distribution\linux\Ryujinx.sh">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\distribution\linux\mime\Ryujinx.xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>mime\Ryujinx.xml</TargetPath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Ui\**\*.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
|
@ -144,9 +144,9 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value)
|
||||
{
|
||||
device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
|
||||
if (_parent.AppHost != null)
|
||||
if (_parent.ViewModel.AppHost != null)
|
||||
{
|
||||
_parent.AppHost.Stop();
|
||||
_parent.ViewModel.AppHost.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,7 +136,7 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_hiddenTextBox.Clear();
|
||||
_parent.RendererControl.Focus();
|
||||
_parent.ViewModel.RendererControl.Focus();
|
||||
|
||||
_parent = null;
|
||||
});
|
||||
|
@ -13,8 +13,8 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
DefaultBackgroundColor = BrushToThemeColor(parent.Background);
|
||||
DefaultForegroundColor = BrushToThemeColor(parent.Foreground);
|
||||
DefaultBorderColor = BrushToThemeColor(parent.BorderBrush);
|
||||
SelectionBackgroundColor = BrushToThemeColor(parent.SearchBox.SelectionBrush);
|
||||
SelectionForegroundColor = BrushToThemeColor(parent.SearchBox.SelectionForegroundBrush);
|
||||
SelectionBackgroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionBrush);
|
||||
SelectionForegroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionForegroundBrush);
|
||||
}
|
||||
|
||||
public string FontFamily { get; }
|
||||
|
@ -139,6 +139,9 @@
|
||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
|
||||
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.GridItemSelectorSize}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
@ -38,9 +38,9 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
if (sender is ListBox listBox)
|
||||
{
|
||||
var selected = listBox.SelectedItem as ApplicationData;
|
||||
_selectedApplication = listBox.SelectedItem as ApplicationData;
|
||||
|
||||
_selectedApplication = selected;
|
||||
(DataContext as MainWindowViewModel).GridSelectedApplication = _selectedApplication;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,12 +137,12 @@
|
||||
<Style Selector="ListBoxItem:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
|
||||
<Setter Property="MinHeight" Value="100" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource AppListHoverBackgroundColor}" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Border#SelectionIndicator">
|
||||
<Setter Property="MinHeight" Value="{Binding $parent[UserControl].DataContext.ListItemSelectorSize}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
@ -38,9 +38,9 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
if (sender is ListBox listBox)
|
||||
{
|
||||
var selected = listBox.SelectedItem as ApplicationData;
|
||||
_selectedApplication = listBox.SelectedItem as ApplicationData;
|
||||
|
||||
_selectedApplication = selected;
|
||||
(DataContext as MainWindowViewModel).ListSelectedApplication = _selectedApplication;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,56 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
private static bool _isChoiceDialogOpen;
|
||||
|
||||
private async static Task<UserResult> ShowContentDialog(
|
||||
public async static Task<UserResult> ShowContentDialog(
|
||||
string title,
|
||||
object content,
|
||||
string primaryButton,
|
||||
string secondaryButton,
|
||||
string closeButton,
|
||||
UserResult primaryButtonResult = UserResult.Ok,
|
||||
ManualResetEvent deferResetEvent = null,
|
||||
Func<Window, Task> doWhileDeferred = null,
|
||||
TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null)
|
||||
{
|
||||
UserResult result = UserResult.None;
|
||||
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Title = title,
|
||||
PrimaryButtonText = primaryButton,
|
||||
SecondaryButtonText = secondaryButton,
|
||||
CloseButtonText = closeButton,
|
||||
Content = content
|
||||
};
|
||||
|
||||
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = primaryButtonResult;
|
||||
});
|
||||
|
||||
contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = UserResult.No;
|
||||
contentDialog.PrimaryButtonClick -= deferCloseAction;
|
||||
});
|
||||
|
||||
contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = UserResult.Cancel;
|
||||
contentDialog.PrimaryButtonClick -= deferCloseAction;
|
||||
});
|
||||
|
||||
if (deferResetEvent != null)
|
||||
{
|
||||
contentDialog.PrimaryButtonClick += deferCloseAction;
|
||||
}
|
||||
|
||||
await ShowAsync(contentDialog);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async static Task<UserResult> ShowTextDialog(
|
||||
string title,
|
||||
string primaryText,
|
||||
string secondaryText,
|
||||
@ -32,119 +81,9 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
Func<Window, Task> doWhileDeferred = null,
|
||||
TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null)
|
||||
{
|
||||
UserResult result = UserResult.None;
|
||||
Grid content = CreateTextDialogContent(primaryText, secondaryText, iconSymbol);
|
||||
|
||||
bool useOverlay = false;
|
||||
Window mainWindow = null;
|
||||
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al)
|
||||
{
|
||||
foreach (var item in al.Windows)
|
||||
{
|
||||
if (item.IsActive && item is MainWindow window && window.ViewModel.IsGameRunning)
|
||||
{
|
||||
mainWindow = window;
|
||||
useOverlay = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentDialog contentDialog = null;
|
||||
ContentDialogOverlayWindow overlay = null;
|
||||
|
||||
if (useOverlay)
|
||||
{
|
||||
overlay = new ContentDialogOverlayWindow()
|
||||
{
|
||||
Height = mainWindow.Bounds.Height,
|
||||
Width = mainWindow.Bounds.Width,
|
||||
Position = mainWindow.PointToScreen(new Point())
|
||||
};
|
||||
|
||||
mainWindow.PositionChanged += OverlayOnPositionChanged;
|
||||
|
||||
void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
|
||||
{
|
||||
overlay.Position = mainWindow.PointToScreen(new Point());
|
||||
}
|
||||
|
||||
contentDialog = overlay.ContentDialog;
|
||||
|
||||
bool opened = false;
|
||||
|
||||
overlay.Opened += OverlayOnActivated;
|
||||
|
||||
async void OverlayOnActivated(object sender, EventArgs e)
|
||||
{
|
||||
if (opened)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
|
||||
overlay.Position = mainWindow.PointToScreen(new Point());
|
||||
|
||||
await ShowDialog();
|
||||
}
|
||||
|
||||
await overlay.ShowDialog(mainWindow);
|
||||
}
|
||||
else
|
||||
{
|
||||
contentDialog = new ContentDialog();
|
||||
|
||||
await ShowDialog();
|
||||
}
|
||||
|
||||
async Task ShowDialog()
|
||||
{
|
||||
contentDialog.Title = title;
|
||||
contentDialog.PrimaryButtonText = primaryButton;
|
||||
contentDialog.SecondaryButtonText = secondaryButton;
|
||||
contentDialog.CloseButtonText = closeButton;
|
||||
contentDialog.Content = CreateDialogTextContent(primaryText, secondaryText, iconSymbol);
|
||||
|
||||
contentDialog.PrimaryButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = primaryButtonResult;
|
||||
});
|
||||
contentDialog.SecondaryButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = UserResult.No;
|
||||
contentDialog.PrimaryButtonClick -= deferCloseAction;
|
||||
});
|
||||
contentDialog.CloseButtonCommand = MiniCommand.Create(() =>
|
||||
{
|
||||
result = UserResult.Cancel;
|
||||
contentDialog.PrimaryButtonClick -= deferCloseAction;
|
||||
});
|
||||
|
||||
if (deferResetEvent != null)
|
||||
{
|
||||
contentDialog.PrimaryButtonClick += deferCloseAction;
|
||||
}
|
||||
|
||||
if (useOverlay)
|
||||
{
|
||||
await contentDialog.ShowAsync(overlay, ContentDialogPlacement.Popup);
|
||||
|
||||
overlay!.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
await contentDialog.ShowAsync(ContentDialogPlacement.Popup);
|
||||
}
|
||||
}
|
||||
|
||||
if (useOverlay)
|
||||
{
|
||||
overlay.Content = null;
|
||||
overlay.Close();
|
||||
}
|
||||
|
||||
return result;
|
||||
return await ShowContentDialog(title, content, primaryButton, secondaryButton, closeButton, primaryButtonResult, deferResetEvent, doWhileDeferred, deferCloseAction);
|
||||
}
|
||||
|
||||
public async static Task<UserResult> ShowDeferredContentDialog(
|
||||
@ -162,7 +101,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
bool startedDeferring = false;
|
||||
UserResult result = UserResult.None;
|
||||
|
||||
return await ShowContentDialog(
|
||||
return await ShowTextDialog(
|
||||
title,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
@ -192,8 +131,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
sender.PrimaryButtonClick -= DeferClose;
|
||||
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
Task.Run(() =>
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
deferResetEvent.WaitOne();
|
||||
|
||||
@ -202,7 +140,6 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
deferral.Complete();
|
||||
});
|
||||
});
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
|
||||
if (doWhileDeferred != null)
|
||||
{
|
||||
@ -213,29 +150,37 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private static Grid CreateDialogTextContent(string primaryText, string secondaryText, int symbol)
|
||||
private static Grid CreateTextDialogContent(string primaryText, string secondaryText, int symbol)
|
||||
{
|
||||
Grid content = new Grid();
|
||||
content.RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() };
|
||||
content.ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() };
|
||||
Grid content = new()
|
||||
{
|
||||
RowDefinitions = new RowDefinitions() { new RowDefinition(), new RowDefinition() },
|
||||
ColumnDefinitions = new ColumnDefinitions() { new ColumnDefinition(GridLength.Auto), new ColumnDefinition() },
|
||||
|
||||
content.MinHeight = 80;
|
||||
MinHeight = 80
|
||||
};
|
||||
|
||||
SymbolIcon icon = new()
|
||||
{
|
||||
Symbol = (Symbol)symbol,
|
||||
Margin = new Thickness(10),
|
||||
FontSize = 40,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
};
|
||||
|
||||
SymbolIcon icon = new SymbolIcon { Symbol = (Symbol)symbol, Margin = new Thickness(10) };
|
||||
icon.FontSize = 40;
|
||||
icon.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
|
||||
Grid.SetColumn(icon, 0);
|
||||
Grid.SetRowSpan(icon, 2);
|
||||
Grid.SetRow(icon, 0);
|
||||
|
||||
TextBlock primaryLabel = new TextBlock()
|
||||
TextBlock primaryLabel = new()
|
||||
{
|
||||
Text = primaryText,
|
||||
Margin = new Thickness(5),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxWidth = 450
|
||||
};
|
||||
TextBlock secondaryLabel = new TextBlock()
|
||||
|
||||
TextBlock secondaryLabel = new()
|
||||
{
|
||||
Text = secondaryText,
|
||||
Margin = new Thickness(5),
|
||||
@ -262,7 +207,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
string closeButton,
|
||||
string title)
|
||||
{
|
||||
return await ShowContentDialog(
|
||||
return await ShowTextDialog(
|
||||
title,
|
||||
primary,
|
||||
secondaryText,
|
||||
@ -280,7 +225,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
string title,
|
||||
UserResult primaryButtonResult = UserResult.Yes)
|
||||
{
|
||||
return await ShowContentDialog(
|
||||
return await ShowTextDialog(
|
||||
string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle] : title,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
@ -298,7 +243,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
internal static async Task CreateUpdaterInfoDialog(string primary, string secondaryText)
|
||||
{
|
||||
await ShowContentDialog(
|
||||
await ShowTextDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterTitle],
|
||||
primary,
|
||||
secondaryText,
|
||||
@ -310,7 +255,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
internal static async Task CreateWarningDialog(string primary, string secondaryText)
|
||||
{
|
||||
await ShowContentDialog(
|
||||
await ShowTextDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogWarningTitle],
|
||||
primary,
|
||||
secondaryText,
|
||||
@ -324,7 +269,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, errorMessage);
|
||||
|
||||
await ShowContentDialog(
|
||||
await ShowTextDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
|
||||
LocaleManager.Instance[LocaleKeys.DialogErrorMessage],
|
||||
errorMessage,
|
||||
@ -343,8 +288,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
_isChoiceDialogOpen = true;
|
||||
|
||||
UserResult response =
|
||||
await ShowContentDialog(
|
||||
UserResult response = await ShowTextDialog(
|
||||
title,
|
||||
primary,
|
||||
secondaryText,
|
||||
@ -396,5 +340,98 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static async Task<ContentDialogResult> ShowAsync(ContentDialog contentDialog)
|
||||
{
|
||||
ContentDialogResult result;
|
||||
|
||||
ContentDialogOverlayWindow contentDialogOverlayWindow = null;
|
||||
|
||||
Window parent = GetMainWindow();
|
||||
|
||||
if (parent.IsActive && parent is MainWindow window && window.ViewModel.IsGameRunning)
|
||||
{
|
||||
contentDialogOverlayWindow = new()
|
||||
{
|
||||
Height = parent.Bounds.Height,
|
||||
Width = parent.Bounds.Width,
|
||||
Position = parent.PointToScreen(new Point()),
|
||||
ShowInTaskbar = false
|
||||
};
|
||||
|
||||
parent.PositionChanged += OverlayOnPositionChanged;
|
||||
|
||||
void OverlayOnPositionChanged(object sender, PixelPointEventArgs e)
|
||||
{
|
||||
contentDialogOverlayWindow.Position = parent.PointToScreen(new Point());
|
||||
}
|
||||
|
||||
contentDialogOverlayWindow.ContentDialog = contentDialog;
|
||||
|
||||
bool opened = false;
|
||||
|
||||
contentDialogOverlayWindow.Opened += OverlayOnActivated;
|
||||
|
||||
async void OverlayOnActivated(object sender, EventArgs e)
|
||||
{
|
||||
if (opened)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
opened = true;
|
||||
|
||||
contentDialogOverlayWindow.Position = parent.PointToScreen(new Point());
|
||||
|
||||
result = await ShowDialog();
|
||||
}
|
||||
|
||||
result = await contentDialogOverlayWindow.ShowDialog<ContentDialogResult>(parent);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await ShowDialog();
|
||||
}
|
||||
|
||||
async Task<ContentDialogResult> ShowDialog()
|
||||
{
|
||||
if (contentDialogOverlayWindow is not null)
|
||||
{
|
||||
result = await contentDialog.ShowAsync(contentDialogOverlayWindow);
|
||||
|
||||
contentDialogOverlayWindow!.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await contentDialog.ShowAsync();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (contentDialogOverlayWindow is not null)
|
||||
{
|
||||
contentDialogOverlayWindow.Content = null;
|
||||
contentDialogOverlayWindow.Close();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Window GetMainWindow()
|
||||
{
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime al)
|
||||
{
|
||||
foreach (Window item in al.Windows)
|
||||
{
|
||||
if (item.IsActive && item is MainWindow window)
|
||||
{
|
||||
return window;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
136
Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs
Normal file
136
Ryujinx.Ava/UI/ViewModels/AboutWindowViewModel.cs
Normal file
@ -0,0 +1,136 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public class AboutWindowViewModel : BaseModel
|
||||
{
|
||||
private Bitmap _githubLogo;
|
||||
private Bitmap _discordLogo;
|
||||
private Bitmap _patreonLogo;
|
||||
private Bitmap _twitterLogo;
|
||||
|
||||
private string _version;
|
||||
private string _supporters;
|
||||
|
||||
public Bitmap GithubLogo
|
||||
{
|
||||
get => _githubLogo;
|
||||
set
|
||||
{
|
||||
_githubLogo = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public Bitmap DiscordLogo
|
||||
{
|
||||
get => _discordLogo;
|
||||
set
|
||||
{
|
||||
_discordLogo = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public Bitmap PatreonLogo
|
||||
{
|
||||
get => _patreonLogo;
|
||||
set
|
||||
{
|
||||
_patreonLogo = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public Bitmap TwitterLogo
|
||||
{
|
||||
get => _twitterLogo;
|
||||
set
|
||||
{
|
||||
_twitterLogo = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Supporters
|
||||
{
|
||||
get => _supporters;
|
||||
set
|
||||
{
|
||||
_supporters = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Version
|
||||
{
|
||||
get => _version;
|
||||
set
|
||||
{
|
||||
_version = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string Developers
|
||||
{
|
||||
get => string.Format(LocaleManager.Instance[LocaleKeys.AboutPageDeveloperListMore], "gdkchan, Ac_K, marysaka, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, GoffyDude, TSRBerry, IsaacMarovitz");
|
||||
}
|
||||
|
||||
public AboutWindowViewModel()
|
||||
{
|
||||
Version = Program.Version;
|
||||
|
||||
var assets = AvaloniaLocator.Current.GetService<Avalonia.Platform.IAssetLoader>();
|
||||
|
||||
if (ConfigurationState.Instance.Ui.BaseStyle.Value == "Light")
|
||||
{
|
||||
GithubLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_GitHub_Light.png?assembly=Ryujinx.Ui.Common")));
|
||||
DiscordLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Discord_Light.png?assembly=Ryujinx.Ui.Common")));
|
||||
PatreonLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Patreon_Light.png?assembly=Ryujinx.Ui.Common")));
|
||||
TwitterLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Twitter_Light.png?assembly=Ryujinx.Ui.Common")));
|
||||
}
|
||||
else
|
||||
{
|
||||
GithubLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_GitHub_Dark.png?assembly=Ryujinx.Ui.Common")));
|
||||
DiscordLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Discord_Dark.png?assembly=Ryujinx.Ui.Common")));
|
||||
PatreonLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Patreon_Dark.png?assembly=Ryujinx.Ui.Common")));
|
||||
TwitterLogo = new Bitmap(assets.Open(new Uri("resm:Ryujinx.Ui.Common.Resources.Logo_Twitter_Dark.png?assembly=Ryujinx.Ui.Common")));
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(DownloadPatronsJson);
|
||||
}
|
||||
|
||||
private async Task DownloadPatronsJson()
|
||||
{
|
||||
if (!NetworkInterface.GetIsNetworkAvailable())
|
||||
{
|
||||
Supporters = LocaleManager.Instance[LocaleKeys.ConnectionError];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HttpClient httpClient = new();
|
||||
|
||||
try
|
||||
{
|
||||
string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
|
||||
|
||||
Supporters = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString)) + "\n\n";
|
||||
}
|
||||
catch
|
||||
{
|
||||
Supporters = LocaleManager.Instance[LocaleKeys.ApiError];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -244,9 +244,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
||||
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
||||
if (_mainWindow.AppHost != null)
|
||||
if (_mainWindow.ViewModel.AppHost != null)
|
||||
{
|
||||
_mainWindow.AppHost.NpadManager.BlockInputUpdates();
|
||||
_mainWindow.ViewModel.AppHost.NpadManager.BlockInputUpdates();
|
||||
}
|
||||
|
||||
_isLoaded = false;
|
||||
@ -862,7 +862,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
_mainWindow.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
|
||||
_mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
|
||||
|
||||
// Atomically replace and signal input change.
|
||||
// NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event.
|
||||
@ -891,7 +891,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
|
||||
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
|
||||
|
||||
_mainWindow.AppHost?.NpadManager.UnblockInputUpdates();
|
||||
_mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates();
|
||||
|
||||
SelectedGamepad?.Dispose();
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
216
Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml
Normal file
216
Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml
Normal file
@ -0,0 +1,216 @@
|
||||
<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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
mc:Ignorable="d"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
x:DataType="viewModels:MainWindowViewModel"
|
||||
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView"
|
||||
x:CompileBindings="True">
|
||||
<Design.DataContext>
|
||||
<viewModels:MainWindowViewModel />
|
||||
</Design.DataContext>
|
||||
<DockPanel HorizontalAlignment="Stretch">
|
||||
<Menu
|
||||
Name="Menu"
|
||||
Height="35"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left">
|
||||
<Menu.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<DockPanel Margin="0" HorizontalAlignment="Stretch" />
|
||||
</ItemsPanelTemplate>
|
||||
</Menu.ItemsPanel>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarFile}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenFile}"
|
||||
Header="{locale:Locale MenuBarFileOpenFromFile}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
ToolTip.Tip="{locale:Locale LoadApplicationFileTooltip}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenFolder}"
|
||||
Header="{locale:Locale MenuBarFileOpenUnpacked}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
ToolTip.Tip="{locale:Locale LoadApplicationFolderTooltip}" />
|
||||
<MenuItem Header="{locale:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}">
|
||||
<MenuItem
|
||||
Click="OpenMiiApplet"
|
||||
Header="Mii Edit Applet"
|
||||
ToolTip.Tip="{locale:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenRyujinxFolder}"
|
||||
Header="{locale:Locale MenuBarFileOpenEmuFolder}"
|
||||
ToolTip.Tip="{locale:Locale OpenRyujinxFolderTooltip}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenLogsFolder}"
|
||||
Header="{locale:Locale MenuBarFileOpenLogsFolder}"
|
||||
ToolTip.Tip="{locale:Locale OpenRyujinxLogsTooltip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="CloseWindow"
|
||||
Header="{locale:Locale MenuBarFileExit}"
|
||||
ToolTip.Tip="{locale:Locale ExitTooltip}" />
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarOptions}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ToggleFullscreen}"
|
||||
Header="{locale:Locale MenuBarOptionsToggleFullscreen}"
|
||||
InputGesture="F11" />
|
||||
<MenuItem>
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}"
|
||||
MinWidth="250">
|
||||
<TextBlock Text="{locale:Locale MenuBarOptionsStartGamesInFullscreen}"/>
|
||||
</CheckBox>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem IsVisible="{Binding ShowConsoleVisible}">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding ShowConsole, Mode=TwoWay}"
|
||||
MinWidth="250">
|
||||
<TextBlock Text="{locale:Locale MenuBarOptionsShowConsole}"/>
|
||||
</CheckBox>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="{locale:Locale MenuBarOptionsChangeLanguage}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="de_DE"
|
||||
Header="Deutsch" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="en_US"
|
||||
Header="English (US)" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="es_ES"
|
||||
Header="Español (ES)" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="fr_FR"
|
||||
Header="Français" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="it_IT"
|
||||
Header="Italiano" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="pt_BR"
|
||||
Header="Português (BR)" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="tr_TR"
|
||||
Header="Türkçe" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="el_GR"
|
||||
Header="Ελληνικά" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="pl_PL"
|
||||
Header="Polski" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="ru_RU"
|
||||
Header="Русский" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="zh_CN"
|
||||
Header="简体中文" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="zh_TW"
|
||||
Header="繁體中文" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="ja_JP"
|
||||
Header="日本語" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="ko_KR"
|
||||
Header="한국어" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="OpenSettings"
|
||||
Header="{locale:Locale MenuBarOptionsSettings}"
|
||||
ToolTip.Tip="{locale:Locale OpenSettingsTooltip}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ManageProfiles}"
|
||||
Header="{locale:Locale MenuBarOptionsManageUserProfiles}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
ToolTip.Tip="{locale:Locale OpenProfileManagerTooltip}" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
Name="ActionsMenuItem"
|
||||
VerticalAlignment="Center"
|
||||
Header="{locale:Locale MenuBarActions}"
|
||||
IsEnabled="{Binding IsGameRunning}">
|
||||
<MenuItem
|
||||
Click="PauseEmulation_Click"
|
||||
Header="{locale:Locale MenuBarOptionsPauseEmulation}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding !IsPaused}"
|
||||
IsVisible="{Binding !IsPaused}" />
|
||||
<MenuItem
|
||||
Click="ResumeEmulation_Click"
|
||||
Header="{locale:Locale MenuBarOptionsResumeEmulation}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding IsPaused}"
|
||||
IsVisible="{Binding IsPaused}" />
|
||||
<MenuItem
|
||||
Click="StopEmulation_Click"
|
||||
Header="{locale:Locale MenuBarOptionsStopEmulation}"
|
||||
InputGesture="Escape"
|
||||
IsEnabled="{Binding IsGameRunning}"
|
||||
ToolTip.Tip="{locale:Locale StopEmulationTooltip}" />
|
||||
<MenuItem Command="{ReflectionBinding SimulateWakeUpMessage}" Header="{locale:Locale MenuBarOptionsSimulateWakeUpMessage}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Name="ScanAmiiboMenuItem"
|
||||
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
|
||||
Click="OpenAmiiboWindow"
|
||||
Header="{locale:Locale MenuBarActionsScanAmiibo}"
|
||||
IsEnabled="{Binding IsAmiiboRequested}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding TakeScreenshot}"
|
||||
Header="{locale:Locale MenuBarFileToolsTakeScreenshot}"
|
||||
InputGesture="{Binding ScreenshotKey}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding HideUi}"
|
||||
Header="{locale:Locale MenuBarFileToolsHideUi}"
|
||||
InputGesture="{Binding ShowUiKey}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
Click="OpenCheatManagerForCurrentApp"
|
||||
Header="{locale:Locale GameListContextMenuManageCheat}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}">
|
||||
<MenuItem Header="{locale:Locale MenuBarToolsInstallFirmware}" IsEnabled="{Binding EnableNonGameRunningControls}">
|
||||
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFile}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromFile}" />
|
||||
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFolder}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarHelp}">
|
||||
<MenuItem
|
||||
Name="UpdateMenuItem"
|
||||
IsEnabled="{Binding CanUpdate}"
|
||||
Click="CheckForUpdates"
|
||||
Header="{locale:Locale MenuBarHelpCheckForUpdates}"
|
||||
ToolTip.Tip="{locale:Locale CheckUpdatesTooltip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="OpenAboutWindow"
|
||||
Header="{locale:Locale MenuBarHelpAbout}"
|
||||
ToolTip.Tip="{locale:Locale OpenAboutTooltip}" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</DockPanel>
|
||||
</UserControl>
|
143
Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs
Normal file
143
Ryujinx.Ava/UI/Views/Main/MainMenuBarView.axaml.cs
Normal file
@ -0,0 +1,143 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using System.Threading.Tasks;
|
||||
using LibHac.FsSystem;
|
||||
using LibHac.Ncm;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.Modules;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Main
|
||||
{
|
||||
public partial class MainMenuBarView : UserControl
|
||||
{
|
||||
public MainWindow Window { get; private set; }
|
||||
public MainWindowViewModel ViewModel { get; private set; }
|
||||
|
||||
public MainMenuBarView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
|
||||
if (this.VisualRoot is MainWindow window)
|
||||
{
|
||||
Window = window;
|
||||
}
|
||||
|
||||
ViewModel = Window.ViewModel;
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
|
||||
private async void StopEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Window.ViewModel.AppHost?.ShowExitPrompt();
|
||||
}
|
||||
|
||||
private async void PauseEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
Window.ViewModel.AppHost?.Pause();
|
||||
});
|
||||
}
|
||||
|
||||
private async void ResumeEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
Window.ViewModel.AppHost?.Resume();
|
||||
});
|
||||
}
|
||||
|
||||
public async void OpenSettings(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager);
|
||||
|
||||
await Window.SettingsWindow.ShowDialog(Window);
|
||||
|
||||
ViewModel.LoadConfigurableHotKeys();
|
||||
}
|
||||
|
||||
public void OpenMiiApplet(object sender, RoutedEventArgs e)
|
||||
{
|
||||
string contentPath = ViewModel.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
|
||||
|
||||
if (!string.IsNullOrEmpty(contentPath))
|
||||
{
|
||||
ViewModel.LoadApplication(contentPath, false, "Mii Applet");
|
||||
}
|
||||
}
|
||||
|
||||
public async void OpenAmiiboWindow(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!ViewModel.IsAmiiboRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ViewModel.AppHost.Device.System.SearchingForAmiibo(out int deviceId))
|
||||
{
|
||||
string titleId = ViewModel.AppHost.Device.Application.TitleIdText.ToUpper();
|
||||
AmiiboWindow window = new(ViewModel.ShowAll, ViewModel.LastScannedAmiiboId, titleId);
|
||||
|
||||
await window.ShowDialog(Window);
|
||||
|
||||
if (window.IsScanned)
|
||||
{
|
||||
ViewModel.ShowAll = window.ViewModel.ShowAllAmiibo;
|
||||
ViewModel.LastScannedAmiiboId = window.ScannedAmiibo.GetId();
|
||||
|
||||
ViewModel.AppHost.Device.System.ScanAmiibo(deviceId, ViewModel.LastScannedAmiiboId, window.ViewModel.UseRandomUuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!ViewModel.IsGameRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationLoader application = ViewModel.AppHost.Device.Application;
|
||||
if (application != null)
|
||||
{
|
||||
await new CheatWindow(Window.VirtualFileSystem, application.TitleIdText, application.TitleName).ShowDialog(Window);
|
||||
|
||||
ViewModel.AppHost.Device.EnableCheats();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem)
|
||||
{
|
||||
ViewModel.IsAmiiboRequested = Window.ViewModel.AppHost.Device.System.SearchingForAmiibo(out _);
|
||||
}
|
||||
}
|
||||
|
||||
public async void CheckForUpdates(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Updater.CanUpdate(true, Window))
|
||||
{
|
||||
await Updater.BeginParse(Window, true);
|
||||
}
|
||||
}
|
||||
|
||||
public async void OpenAboutWindow(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await AboutWindow.Show();
|
||||
}
|
||||
|
||||
public void CloseWindow(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Window.Close();
|
||||
}
|
||||
}
|
||||
}
|
232
Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml
Normal file
232
Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml
Normal file
@ -0,0 +1,232 @@
|
||||
<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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.Views.Main.MainStatusBarView"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:MainWindowViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:MainWindowViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid
|
||||
Name="StatusBar"
|
||||
Margin="0"
|
||||
MinHeight="22"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}"
|
||||
DockPanel.Dock="Bottom"
|
||||
IsVisible="{Binding ShowMenuAndStatusBar}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
Margin="5"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}">
|
||||
<Grid Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Width="25"
|
||||
Height="25"
|
||||
MinWidth="0"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
Command="{ReflectionBinding LoadApplications}">
|
||||
<ui:SymbolIcon
|
||||
Width="50"
|
||||
Height="100"
|
||||
Symbol="Refresh" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
Name="LoadStatus"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}"
|
||||
Text="{locale:Locale StatusBarGamesLoaded}" />
|
||||
<ProgressBar
|
||||
Name="LoadProgressBar"
|
||||
Grid.Column="2"
|
||||
Height="6"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource HighlightColor}"
|
||||
IsVisible="{Binding StatusBarVisible}"
|
||||
Maximum="{Binding StatusBarProgressMaximum}"
|
||||
Value="{Binding StatusBarProgressValue}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="0,2"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsGameRunning}"
|
||||
MaxHeight="18"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Name="VsyncStatus"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{Binding VsyncColor}"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="VsyncStatus_PointerReleased"
|
||||
Text="VSync"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Name="DockedStatus"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="DockedStatus_PointerReleased"
|
||||
Text="{Binding DockedStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Name="AspectRatioStatus"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="AspectRatioStatus_PointerReleased"
|
||||
Text="{Binding AspectRatioStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<ui:ToggleSplitButton
|
||||
Name="VolumeStatus"
|
||||
Padding="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}"
|
||||
BorderThickness="0"
|
||||
Content="{Binding VolumeStatusText}"
|
||||
IsChecked="{Binding VolumeMuted}"
|
||||
IsVisible="{Binding !ShowLoadProgress}">
|
||||
<ui:ToggleSplitButton.Flyout>
|
||||
<Flyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
|
||||
<Grid Margin="0">
|
||||
<Slider
|
||||
MaxHeight="40"
|
||||
Width="150"
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="0.05"
|
||||
Maximum="1"
|
||||
Minimum="0"
|
||||
SmallChange="0.01"
|
||||
TickFrequency="0.05"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Value="{Binding Volume}" />
|
||||
</Grid>
|
||||
</Flyout>
|
||||
</ui:ToggleSplitButton.Flyout>
|
||||
</ui:ToggleSplitButton>
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding GameStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding FifoStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding BackendText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding GpuNameText}"
|
||||
TextAlignment="Left" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="3"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding ShowFirmwareStatus}"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Name="FirmwareStatus"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Text="{locale:Locale StatusBarSystemVersion}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
52
Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs
Normal file
52
Ryujinx.Ava/UI/Views/Main/MainStatusBarView.axaml.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Main
|
||||
{
|
||||
public partial class MainStatusBarView : UserControl
|
||||
{
|
||||
public MainWindow Window;
|
||||
|
||||
public MainStatusBarView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
|
||||
if (VisualRoot is MainWindow window)
|
||||
{
|
||||
Window = window;
|
||||
}
|
||||
|
||||
DataContext = Window.ViewModel;
|
||||
}
|
||||
|
||||
private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
Window.ViewModel.AppHost.Device.EnableDeviceVsync = !Window.ViewModel.AppHost.Device.EnableDeviceVsync;
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {Window.ViewModel.AppHost.Device.EnableDeviceVsync}");
|
||||
}
|
||||
|
||||
private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
|
||||
}
|
||||
|
||||
private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;
|
||||
|
||||
ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
|
||||
}
|
||||
}
|
||||
}
|
176
Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml
Normal file
176
Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml
Normal file
@ -0,0 +1,176 @@
|
||||
<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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.UI.Views.Main.MainViewControls"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:MainWindowViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:MainWindowViewModel />
|
||||
</Design.DataContext>
|
||||
<DockPanel
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Button
|
||||
Width="40"
|
||||
MinWidth="40"
|
||||
Margin="5,2,0,2"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{ReflectionBinding SetListMode}"
|
||||
IsEnabled="{Binding IsGrid}">
|
||||
<ui:FontIcon
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
|
||||
Glyph="{helpers:GlyphValueConverter List}" />
|
||||
</Button>
|
||||
<Button
|
||||
Width="40"
|
||||
MinWidth="40"
|
||||
Margin="5,2,5,2"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{ReflectionBinding SetGridMode}"
|
||||
IsEnabled="{Binding IsList}">
|
||||
<ui:FontIcon
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
|
||||
Glyph="{helpers:GlyphValueConverter Grid}" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{locale:Locale IconSize}"
|
||||
ToolTip.Tip="{locale:Locale IconSizeTooltip}" />
|
||||
<Slider
|
||||
Width="150"
|
||||
Height="35"
|
||||
Margin="5,-10,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="4"
|
||||
Minimum="1"
|
||||
TickFrequency="1"
|
||||
ToolTip.Tip="{locale:Locale IconSizeTooltip}"
|
||||
Value="{Binding GridSizeScale}" />
|
||||
<CheckBox
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding ShowNames, Mode=TwoWay}"
|
||||
IsVisible="{Binding IsGrid}">
|
||||
<TextBlock Margin="5,3,0,0" Text="{locale:Locale CommonShowNames}" />
|
||||
</CheckBox>
|
||||
<TextBox
|
||||
Name="SearchBox"
|
||||
MinWidth="200"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
KeyUp="SearchBox_OnKeyUp"
|
||||
Text="{Binding SearchText}"
|
||||
Watermark="{locale:Locale MenuSearch}" />
|
||||
<ui:DropDownButton
|
||||
Width="150"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content="{Binding SortName}"
|
||||
DockPanel.Dock="Right">
|
||||
<ui:DropDownButton.Flyout>
|
||||
<Flyout Placement="Bottom">
|
||||
<StackPanel
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<StackPanel>
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale CommonFavorite}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByFavorite, Mode=OneTime}"
|
||||
Tag="Favorite" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderApplication}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
|
||||
Tag="Title" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderDeveloper}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByDeveloper, Mode=OneTime}"
|
||||
Tag="Developer" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderTimePlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByTimePlayed, Mode=OneTime}"
|
||||
Tag="TotalTimePlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderLastPlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByLastPlayed, Mode=OneTime}"
|
||||
Tag="LastPlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderFileExtension}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByType, Mode=OneTime}"
|
||||
Tag="FileType" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderFileSize}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedBySize, Mode=OneTime}"
|
||||
Tag="FileSize" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderPath}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByPath, Mode=OneTime}"
|
||||
Tag="Path" />
|
||||
</StackPanel>
|
||||
<Border
|
||||
Width="60"
|
||||
Height="2"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
BorderBrush="White"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Separator Height="0" HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
<RadioButton
|
||||
Checked="Order_Checked"
|
||||
Content="{locale:Locale OrderAscending}"
|
||||
GroupName="Order"
|
||||
IsChecked="{Binding IsAscending, Mode=OneTime}"
|
||||
Tag="Ascending" />
|
||||
<RadioButton
|
||||
Checked="Order_Checked"
|
||||
Content="{locale:Locale OrderDescending}"
|
||||
GroupName="Order"
|
||||
IsChecked="{Binding !IsAscending, Mode=OneTime}"
|
||||
Tag="Descending" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</ui:DropDownButton.Flyout>
|
||||
</ui:DropDownButton>
|
||||
<TextBlock
|
||||
Margin="10,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Text="{locale:Locale CommonSort}" />
|
||||
</DockPanel>
|
||||
</UserControl>
|
54
Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs
Normal file
54
Ryujinx.Ava/UI/Views/Main/MainViewControls.axaml.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.Common;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Main
|
||||
{
|
||||
public partial class MainViewControls : UserControl
|
||||
{
|
||||
public MainWindowViewModel ViewModel;
|
||||
|
||||
public MainViewControls()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
|
||||
if (VisualRoot is MainWindow window)
|
||||
{
|
||||
ViewModel = window.ViewModel;
|
||||
}
|
||||
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
|
||||
public void Sort_Checked(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is RadioButton button)
|
||||
{
|
||||
ViewModel.Sort(Enum.Parse<ApplicationSort>(button.Tag.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
public void Order_Checked(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is RadioButton button)
|
||||
{
|
||||
ViewModel.Sort(button.Tag.ToString() != "Descending");
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
ViewModel.SearchText = SearchBox.Text;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,282 +1,247 @@
|
||||
<window:StyleableWindow
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Windows.AboutWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
Width="850"
|
||||
Height="550"
|
||||
MinWidth="500"
|
||||
MinHeight="550"
|
||||
d:DesignHeight="350"
|
||||
d:DesignWidth="400"
|
||||
CanResize="False"
|
||||
SizeToContent="Width"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
mc:Ignorable="d"
|
||||
Focusable="True">
|
||||
<Grid
|
||||
Margin="15"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:viewModel="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
Width="550"
|
||||
Height="260"
|
||||
Margin="0,-12,0,0"
|
||||
d:DesignHeight="260"
|
||||
d:DesignWidth="550"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModel:AboutWindowViewModel"
|
||||
Focusable="True"
|
||||
mc:Ignorable="d">
|
||||
<Design.DataContext>
|
||||
<viewModel:AboutWindowViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="0">
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Spacing="10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="0"
|
||||
Height="110"
|
||||
MinWidth="50"
|
||||
Margin="5,10,20,10"
|
||||
Height="80"
|
||||
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" />
|
||||
<flex:FlexPanel
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Direction="Column"
|
||||
JustifyContent="SpaceAround"
|
||||
RowSpacing="2">
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="0,20,0,0"
|
||||
FontSize="35"
|
||||
FontSize="28"
|
||||
FontWeight="Bold"
|
||||
Text="Ryujinx"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,0,0"
|
||||
FontSize="16"
|
||||
Text="(REE-YOU-JINX)"
|
||||
TextAlignment="Center" />
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://www.ryujinx.org/">
|
||||
<TextBlock
|
||||
Text="www.ryujinx.org"
|
||||
TextAlignment="Center"
|
||||
TextDecorations="Underline"
|
||||
ToolTip.Tip="{locale:Locale AboutUrlTooltipMessage}" />
|
||||
</Button>
|
||||
TextAlignment="Left" />
|
||||
<TextBlock Text="(REE-YOU-JINX)" TextAlignment="Left" />
|
||||
</flex:FlexPanel>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
LineHeight="12"
|
||||
Text="{Binding Version}"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Spacing="10">
|
||||
<TextBlock
|
||||
Width="200"
|
||||
HorizontalAlignment="Center"
|
||||
MaxLines="2"
|
||||
FontSize="10"
|
||||
LineHeight="12"
|
||||
Text="{locale:Locale AboutDisclaimerMessage}"
|
||||
TextAlignment="Center" />
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Name="AmiiboLabel"
|
||||
Grid.Row="3"
|
||||
Margin="20"
|
||||
Width="200"
|
||||
HorizontalAlignment="Center"
|
||||
MaxLines="2"
|
||||
FontSize="10"
|
||||
LineHeight="12"
|
||||
PointerPressed="AmiiboLabel_OnPointerPressed"
|
||||
Text="{locale:Locale AboutAmiiboDisclaimerMessage}"
|
||||
TextAlignment="Center" />
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<StackPanel
|
||||
Grid.Row="4"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutPatreonUrlTooltipMessage}">
|
||||
<Button
|
||||
Height="65"
|
||||
MinWidth="30"
|
||||
MinHeight="30"
|
||||
MaxWidth="30"
|
||||
MaxHeight="30"
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://www.patreon.com/ryujinx">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Patreon.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="Patreon" />
|
||||
</Grid>
|
||||
CornerRadius="15"
|
||||
Tag="https://www.patreon.com/ryujinx"
|
||||
ToolTip.Tip="{locale:Locale AboutPatreonUrlTooltipMessage}">
|
||||
<Image Source="{Binding PatreonLogo}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutGithubUrlTooltipMessage}">
|
||||
<Button
|
||||
Height="65"
|
||||
MinWidth="30"
|
||||
MinHeight="30"
|
||||
MaxWidth="30"
|
||||
MaxHeight="30"
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://github.com/Ryujinx/Ryujinx">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_GitHub.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="GitHub" />
|
||||
</Grid>
|
||||
CornerRadius="15"
|
||||
Tag="https://github.com/Ryujinx/Ryujinx"
|
||||
ToolTip.Tip="{locale:Locale AboutGithubUrlTooltipMessage}">
|
||||
<Image Source="{Binding GithubLogo}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutDiscordUrlTooltipMessage}">
|
||||
<Button
|
||||
Height="65"
|
||||
MinWidth="30"
|
||||
MinHeight="30"
|
||||
MaxWidth="30"
|
||||
MaxHeight="30"
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://discordapp.com/invite/N2FmfVc">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Discord.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="Discord" />
|
||||
</Grid>
|
||||
CornerRadius="15"
|
||||
Tag="https://discordapp.com/invite/N2FmfVc"
|
||||
ToolTip.Tip="{locale:Locale AboutDiscordUrlTooltipMessage}">
|
||||
<Image Source="{Binding DiscordLogo}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutTwitterUrlTooltipMessage}">
|
||||
<Button
|
||||
Height="65"
|
||||
MinWidth="30"
|
||||
MinHeight="30"
|
||||
MaxWidth="30"
|
||||
MaxHeight="30"
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://twitter.com/RyujinxEmu">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Source="resm:Ryujinx.Ui.Common.Resources.Logo_Twitter.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="0,5,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="Twitter" />
|
||||
</Grid>
|
||||
CornerRadius="15"
|
||||
Tag="https://twitter.com/RyujinxEmu"
|
||||
ToolTip.Tip="{locale:Locale AboutTwitterUrlTooltipMessage}">
|
||||
<Image Source="{Binding TwitterLogo}" />
|
||||
</Button>
|
||||
<Button
|
||||
MinWidth="30"
|
||||
MinHeight="30"
|
||||
MaxWidth="30"
|
||||
MaxHeight="30"
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
CornerRadius="15"
|
||||
Tag="https://www.ryujinx.org"
|
||||
ToolTip.Tip="{locale:Locale AboutUrlTooltipMessage}">
|
||||
<ui:SymbolIcon Foreground="{DynamicResource ThemeForegroundColor}" Symbol="Link" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Width="2"
|
||||
Margin="5"
|
||||
Width="1"
|
||||
Margin="20,0"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="White"
|
||||
BorderThickness="1,0,0,0">
|
||||
<Separator Width="0" />
|
||||
</Border>
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1,0,0,0" />
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale AboutRyujinxAboutTitle}"
|
||||
TextDecorations="Underline" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="20,5,5,5"
|
||||
LineHeight="20"
|
||||
Text="{locale:Locale AboutRyujinxAboutContent}" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="0,10,0,0"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale AboutRyujinxMaintainersTitle}"
|
||||
TextDecorations="Underline" />
|
||||
Spacing="2">
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
Margin="20,5,5,5"
|
||||
LineHeight="20"
|
||||
Text="{Binding Developers}" />
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale AboutRyujinxAboutTitle}" />
|
||||
<TextBlock
|
||||
FontSize="10"
|
||||
Text="{locale:Locale AboutRyujinxAboutContent}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0,10,0,0"
|
||||
Spacing="2">
|
||||
<TextBlock
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale AboutRyujinxMaintainersTitle}" />
|
||||
<TextBlock
|
||||
FontSize="10"
|
||||
Text="{Binding Developers}"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
Grid.Row="4"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="5"
|
||||
HorizontalAlignment="Left"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a">
|
||||
<TextBlock
|
||||
FontSize="10"
|
||||
Text="{locale:Locale AboutRyujinxContributorsButtonHeader}"
|
||||
TextAlignment="Right"
|
||||
TextDecorations="Underline"
|
||||
ToolTip.Tip="{locale:Locale AboutRyujinxMaintainersContentTooltipMessage}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,10,0,0"
|
||||
Spacing="2">
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Margin="0,0,0,0"
|
||||
FontSize="15"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale AboutRyujinxSupprtersTitle}"
|
||||
TextDecorations="Underline" />
|
||||
<Border
|
||||
Grid.Row="6"
|
||||
Width="460"
|
||||
Height="200"
|
||||
Margin="20,5"
|
||||
Padding="5"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="White"
|
||||
BorderThickness="1">
|
||||
Text="{locale:Locale AboutRyujinxSupprtersTitle}" />
|
||||
<ScrollViewer
|
||||
Height="70"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Visible">
|
||||
<TextBlock
|
||||
Name="SupportersTextBlock"
|
||||
VerticalAlignment="Top"
|
||||
FontSize="10"
|
||||
Text="{Binding Supporters}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
||||
</UserControl>
|
@ -1,38 +1,47 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Threading.Tasks;
|
||||
using Button = Avalonia.Controls.Button;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class AboutWindow : StyleableWindow
|
||||
public partial class AboutWindow : UserControl
|
||||
{
|
||||
public AboutWindow()
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
Title = $"Ryujinx {Program.Version} - " + LocaleManager.Instance[LocaleKeys.MenuBarHelpAbout];
|
||||
}
|
||||
|
||||
Version = Program.Version;
|
||||
|
||||
DataContext = this;
|
||||
DataContext = new AboutWindowViewModel();
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
_ = DownloadPatronsJson();
|
||||
}
|
||||
|
||||
public string Supporters { get; set; }
|
||||
public string Version { get; set; }
|
||||
public static async Task Show()
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
|
||||
Content = new AboutWindow()
|
||||
};
|
||||
|
||||
public string Developers => string.Format(LocaleManager.Instance[LocaleKeys.AboutPageDeveloperListMore], "gdkchan, Ac_K, Thog, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, Xpl0itR, GoffyDude, »jD«");
|
||||
Style closeButton = new(x => x.Name("CloseButton"));
|
||||
closeButton.Setters.Add(new Setter(WidthProperty, 80d));
|
||||
|
||||
Style closeButtonParent = new(x => x.Name("CommandSpace"));
|
||||
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, Avalonia.Layout.HorizontalAlignment.Right));
|
||||
|
||||
contentDialog.Styles.Add(closeButton);
|
||||
contentDialog.Styles.Add(closeButtonParent);
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
}
|
||||
|
||||
private void Button_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
@ -42,31 +51,6 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadPatronsJson()
|
||||
{
|
||||
if (!NetworkInterface.GetIsNetworkAvailable())
|
||||
{
|
||||
Supporters = LocaleManager.Instance[LocaleKeys.ConnectionError];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
HttpClient httpClient = new();
|
||||
|
||||
try
|
||||
{
|
||||
string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
|
||||
|
||||
Supporters = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString));
|
||||
}
|
||||
catch
|
||||
{
|
||||
Supporters = LocaleManager.Instance[LocaleKeys.ApiError];
|
||||
}
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() => SupportersTextBlock.Text = Supporters);
|
||||
}
|
||||
|
||||
private void AmiiboLabel_OnPointerPressed(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender is TextBlock)
|
||||
|
@ -2,15 +2,16 @@
|
||||
x:Class="Ryujinx.Ava.UI.Windows.MainWindow"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
Title="Ryujinx"
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
xmlns:main="clr-namespace:Ryujinx.Ava.UI.Views.Main"
|
||||
Cursor="{Binding Cursor}"
|
||||
Title="{Binding Title}"
|
||||
WindowState="{Binding WindowState}"
|
||||
Width="1280"
|
||||
Height="777"
|
||||
MinWidth="1092"
|
||||
@ -66,206 +67,8 @@
|
||||
VerticalAlignment="Stretch"
|
||||
IsVisible="{Binding ShowMenuAndStatusBar}"
|
||||
Orientation="Vertical">
|
||||
<DockPanel HorizontalAlignment="Stretch">
|
||||
<Menu
|
||||
Name="Menu"
|
||||
Height="35"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Left">
|
||||
<Menu.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<DockPanel Margin="0" HorizontalAlignment="Stretch" />
|
||||
</ItemsPanelTemplate>
|
||||
</Menu.ItemsPanel>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarFile}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenFile}"
|
||||
Header="{locale:Locale MenuBarFileOpenFromFile}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
ToolTip.Tip="{locale:Locale LoadApplicationFileTooltip}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenFolder}"
|
||||
Header="{locale:Locale MenuBarFileOpenUnpacked}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
ToolTip.Tip="{locale:Locale LoadApplicationFolderTooltip}" />
|
||||
<MenuItem Header="{locale:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenMiiApplet}"
|
||||
Header="Mii Edit Applet"
|
||||
ToolTip.Tip="{locale:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenRyujinxFolder}"
|
||||
Header="{locale:Locale MenuBarFileOpenEmuFolder}"
|
||||
ToolTip.Tip="{locale:Locale OpenRyujinxFolderTooltip}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenLogsFolder}"
|
||||
Header="{locale:Locale MenuBarFileOpenLogsFolder}"
|
||||
ToolTip.Tip="{locale:Locale OpenRyujinxLogsTooltip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding CloseWindow}"
|
||||
Header="{locale:Locale MenuBarFileExit}"
|
||||
ToolTip.Tip="{locale:Locale ExitTooltip}" />
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarOptions}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ToggleFullscreen}"
|
||||
Header="{locale:Locale MenuBarOptionsToggleFullscreen}"
|
||||
InputGesture="F11" />
|
||||
<MenuItem>
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}"
|
||||
MinWidth="250">
|
||||
<TextBlock Text="{locale:Locale MenuBarOptionsStartGamesInFullscreen}"/>
|
||||
</CheckBox>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem IsVisible="{Binding ShowConsoleVisible}">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox IsChecked="{Binding ShowConsole, Mode=TwoWay}"
|
||||
MinWidth="250">
|
||||
<TextBlock Text="{locale:Locale MenuBarOptionsShowConsole}"/>
|
||||
</CheckBox>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="{locale:Locale MenuBarOptionsChangeLanguage}">
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="de_DE"
|
||||
Header="Deutsch" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="en_US"
|
||||
Header="English (US)" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="es_ES"
|
||||
Header="Español (ES)" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="fr_FR"
|
||||
Header="Français" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="it_IT"
|
||||
Header="Italiano" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="pt_BR"
|
||||
Header="Português (BR)" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="tr_TR"
|
||||
Header="Türkçe" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="el_GR"
|
||||
Header="Ελληνικά" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="pl_PL"
|
||||
Header="Polski" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="ru_RU"
|
||||
Header="Русский" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="zh_CN"
|
||||
Header="简体中文" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="zh_TW"
|
||||
Header="繁體中文" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="ja_JP"
|
||||
Header="日本語" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ChangeLanguage}"
|
||||
CommandParameter="ko_KR"
|
||||
Header="한국어" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenSettings}"
|
||||
Header="{locale:Locale MenuBarOptionsSettings}"
|
||||
ToolTip.Tip="{locale:Locale OpenSettingsTooltip}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding ManageProfiles}"
|
||||
Header="{locale:Locale MenuBarOptionsManageUserProfiles}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
ToolTip.Tip="{locale:Locale OpenProfileManagerTooltip}" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
Name="ActionsMenuItem"
|
||||
VerticalAlignment="Center"
|
||||
Header="{locale:Locale MenuBarActions}"
|
||||
IsEnabled="{Binding IsGameRunning}">
|
||||
<MenuItem
|
||||
Click="PauseEmulation_Click"
|
||||
Header="{locale:Locale MenuBarOptionsPauseEmulation}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding !IsPaused}"
|
||||
IsVisible="{Binding !IsPaused}" />
|
||||
<MenuItem
|
||||
Click="ResumeEmulation_Click"
|
||||
Header="{locale:Locale MenuBarOptionsResumeEmulation}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding IsPaused}"
|
||||
IsVisible="{Binding IsPaused}" />
|
||||
<MenuItem
|
||||
Click="StopEmulation_Click"
|
||||
Header="{locale:Locale MenuBarOptionsStopEmulation}"
|
||||
InputGesture="Escape"
|
||||
IsEnabled="{Binding IsGameRunning}"
|
||||
ToolTip.Tip="{locale:Locale StopEmulationTooltip}" />
|
||||
<MenuItem Command="{ReflectionBinding SimulateWakeUpMessage}" Header="{locale:Locale MenuBarOptionsSimulateWakeUpMessage}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Name="ScanAmiiboMenuItem"
|
||||
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
|
||||
Command="{ReflectionBinding OpenAmiiboWindow}"
|
||||
Header="{locale:Locale MenuBarActionsScanAmiibo}"
|
||||
IsEnabled="{Binding IsAmiiboRequested}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding TakeScreenshot}"
|
||||
Header="{locale:Locale MenuBarFileToolsTakeScreenshot}"
|
||||
InputGesture="{Binding ScreenshotKey}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding HideUi}"
|
||||
Header="{locale:Locale MenuBarFileToolsHideUi}"
|
||||
InputGesture="{Binding ShowUiKey}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenCheatManagerForCurrentApp}"
|
||||
Header="{locale:Locale GameListContextMenuManageCheat}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarTools}">
|
||||
<MenuItem Header="{locale:Locale MenuBarToolsInstallFirmware}" IsEnabled="{Binding EnableNonGameRunningControls}">
|
||||
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFile}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromFile}" />
|
||||
<MenuItem Command="{ReflectionBinding InstallFirmwareFromFolder}" Header="{locale:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{locale:Locale MenuBarHelp}">
|
||||
<MenuItem
|
||||
Name="UpdateMenuItem"
|
||||
Command="{ReflectionBinding CheckForUpdates}"
|
||||
Header="{locale:Locale MenuBarHelpCheckForUpdates}"
|
||||
ToolTip.Tip="{locale:Locale CheckUpdatesTooltip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{ReflectionBinding OpenAboutWindow}"
|
||||
Header="{locale:Locale MenuBarHelpAbout}"
|
||||
ToolTip.Tip="{locale:Locale OpenAboutTooltip}" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</DockPanel>
|
||||
<main:MainMenuBarView
|
||||
Name="MenuBarView" />
|
||||
</StackPanel>
|
||||
<ContentControl
|
||||
Name="MainContent"
|
||||
@ -277,170 +80,14 @@
|
||||
BorderThickness="0,0,0,0"
|
||||
DockPanel.Dock="Top"
|
||||
IsVisible="{Binding ShowContent}">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Name="GameLibrary">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<DockPanel
|
||||
Grid.Row="0"
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Button
|
||||
Width="40"
|
||||
MinWidth="40"
|
||||
Margin="5,2,0,2"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{ReflectionBinding SetListMode}"
|
||||
IsEnabled="{Binding IsGrid}">
|
||||
<ui:FontIcon
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
|
||||
Glyph="{helpers:GlyphValueConverter List}" />
|
||||
</Button>
|
||||
<Button
|
||||
Width="40"
|
||||
MinWidth="40"
|
||||
Margin="5,2,5,2"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{ReflectionBinding SetGridMode}"
|
||||
IsEnabled="{Binding IsList}">
|
||||
<ui:FontIcon
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
|
||||
Glyph="{helpers:GlyphValueConverter Grid}" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
Margin="10,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{locale:Locale IconSize}"
|
||||
ToolTip.Tip="{locale:Locale IconSizeTooltip}" />
|
||||
<Slider
|
||||
Width="150"
|
||||
Height="35"
|
||||
Margin="5,-10,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="4"
|
||||
Minimum="1"
|
||||
TickFrequency="1"
|
||||
ToolTip.Tip="{locale:Locale IconSizeTooltip}"
|
||||
Value="{Binding GridSizeScale}" />
|
||||
<CheckBox
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding ShowNames, Mode=TwoWay}"
|
||||
IsVisible="{Binding IsGrid}">
|
||||
<TextBlock Margin="5,3,0,0" Text="{locale:Locale CommonShowNames}" />
|
||||
</CheckBox>
|
||||
<TextBox
|
||||
Name="SearchBox"
|
||||
MinWidth="200"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
KeyUp="SearchBox_OnKeyUp"
|
||||
Text="{Binding SearchText}"
|
||||
Watermark="{locale:Locale MenuSearch}" />
|
||||
<ui:DropDownButton
|
||||
Width="150"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Content="{Binding SortName}"
|
||||
DockPanel.Dock="Right">
|
||||
<ui:DropDownButton.Flyout>
|
||||
<Flyout Placement="Bottom">
|
||||
<StackPanel
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<StackPanel>
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale CommonFavorite}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByFavorite, Mode=OneTime}"
|
||||
Tag="Favorite" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderApplication}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
|
||||
Tag="Title" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderDeveloper}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByDeveloper, Mode=OneTime}"
|
||||
Tag="Developer" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderTimePlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByTimePlayed, Mode=OneTime}"
|
||||
Tag="TotalTimePlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderLastPlayed}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByLastPlayed, Mode=OneTime}"
|
||||
Tag="LastPlayed" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderFileExtension}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByType, Mode=OneTime}"
|
||||
Tag="FileType" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderFileSize}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedBySize, Mode=OneTime}"
|
||||
Tag="FileSize" />
|
||||
<RadioButton
|
||||
Checked="Sort_Checked"
|
||||
Content="{locale:Locale GameListHeaderPath}"
|
||||
GroupName="Sort"
|
||||
IsChecked="{Binding IsSortedByPath, Mode=OneTime}"
|
||||
Tag="Path" />
|
||||
</StackPanel>
|
||||
<Border
|
||||
Width="60"
|
||||
Height="2"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
BorderBrush="White"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Separator Height="0" HorizontalAlignment="Stretch" />
|
||||
</Border>
|
||||
<RadioButton
|
||||
Checked="Order_Checked"
|
||||
Content="{locale:Locale OrderAscending}"
|
||||
GroupName="Order"
|
||||
IsChecked="{Binding IsAscending, Mode=OneTime}"
|
||||
Tag="Ascending" />
|
||||
<RadioButton
|
||||
Checked="Order_Checked"
|
||||
Content="{locale:Locale OrderDescending}"
|
||||
GroupName="Order"
|
||||
IsChecked="{Binding !IsAscending, Mode=OneTime}"
|
||||
Tag="Descending" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</ui:DropDownButton.Flyout>
|
||||
</ui:DropDownButton>
|
||||
<TextBlock
|
||||
Margin="10,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
Text="{locale:Locale CommonSort}" />
|
||||
</DockPanel>
|
||||
<main:MainViewControls
|
||||
Name="ViewControls"
|
||||
Grid.Row="0"/>
|
||||
<controls:GameListView
|
||||
x:Name="GameList"
|
||||
Grid.Row="1"
|
||||
@ -512,7 +159,8 @@
|
||||
IsVisible="{Binding ShowLoadProgress}"
|
||||
Text="{Binding LoadHeading}"
|
||||
TextAlignment="Left"
|
||||
TextWrapping="Wrap" />
|
||||
TextWrapping="Wrap"
|
||||
MaxWidth="500" />
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="10"
|
||||
@ -529,7 +177,6 @@
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{Binding ProgressBarBackgroundColor}"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5"
|
||||
Foreground="{Binding ProgressBarForegroundColor}"
|
||||
@ -545,226 +192,14 @@
|
||||
FontSize="18"
|
||||
IsVisible="{Binding ShowLoadProgress}"
|
||||
Text="{Binding CacheLoadStatus}"
|
||||
TextAlignment="Left" />
|
||||
TextAlignment="Left"
|
||||
MaxWidth="500" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
Name="StatusBar"
|
||||
Grid.Row="2"
|
||||
Margin="0"
|
||||
MinHeight="22"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}"
|
||||
DockPanel.Dock="Bottom"
|
||||
IsVisible="{Binding ShowMenuAndStatusBar}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
Margin="5"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}">
|
||||
<Grid Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Width="25"
|
||||
Height="25"
|
||||
MinWidth="0"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
Command="{ReflectionBinding LoadApplications}">
|
||||
<ui:SymbolIcon
|
||||
Width="50"
|
||||
Height="100"
|
||||
Symbol="Refresh" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
Name="LoadStatus"
|
||||
Grid.Column="1"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}"
|
||||
Text="{locale:Locale StatusBarGamesLoaded}" />
|
||||
<ProgressBar
|
||||
Name="LoadProgressBar"
|
||||
Grid.Column="2"
|
||||
Height="6"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{DynamicResource HighlightColor}"
|
||||
IsVisible="{Binding EnableNonGameRunningControls}"
|
||||
Maximum="{Binding StatusBarProgressMaximum}"
|
||||
Value="{Binding StatusBarProgressValue}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="0,2"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding IsGameRunning}"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Name="VsyncStatus"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{Binding VsyncColor}"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="VsyncStatus_PointerReleased"
|
||||
Text="VSync"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Name="DockedStatus"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="DockedStatus_PointerReleased"
|
||||
Text="{Binding DockedStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Name="AspectRatioStatus"
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
PointerReleased="AspectRatioStatus_PointerReleased"
|
||||
Text="{Binding AspectRatioStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<ui:ToggleSplitButton
|
||||
Name="VolumeStatus"
|
||||
Padding="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}"
|
||||
BorderThickness="0"
|
||||
Content="{Binding VolumeStatusText}"
|
||||
IsChecked="{Binding VolumeMuted}"
|
||||
IsVisible="{Binding !ShowLoadProgress}">
|
||||
<ui:ToggleSplitButton.Flyout>
|
||||
<Flyout Placement="Bottom" ShowMode="TransientWithDismissOnPointerMoveAway">
|
||||
<Grid Margin="0">
|
||||
<Slider
|
||||
MaxHeight="40"
|
||||
Width="150"
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="0.05"
|
||||
Maximum="1"
|
||||
Minimum="0"
|
||||
SmallChange="0.01"
|
||||
TickFrequency="0.05"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Value="{Binding Volume}" />
|
||||
</Grid>
|
||||
</Flyout>
|
||||
</ui:ToggleSplitButton.Flyout>
|
||||
</ui:ToggleSplitButton>
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding GameStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding FifoStatusText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding BackendText}"
|
||||
TextAlignment="Left" />
|
||||
<Border
|
||||
Width="2"
|
||||
Height="12"
|
||||
Margin="0"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
IsVisible="{Binding !ShowLoadProgress}" />
|
||||
<TextBlock
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding !ShowLoadProgress}"
|
||||
Text="{Binding GpuNameText}"
|
||||
TextAlignment="Left" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Column="3"
|
||||
Margin="0,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding ShowFirmwareStatus}"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Name="FirmwareStatus"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Text="{locale:Locale StatusBarSystemVersion}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<main:MainStatusBarView
|
||||
Name="StatusBarView"
|
||||
Grid.Row="2" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
@ -1,19 +1,14 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Applet;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Gpu;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
@ -25,11 +20,9 @@ using Ryujinx.Ui.App.Common;
|
||||
using Ryujinx.Ui.Common;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
||||
|
||||
@ -38,19 +31,14 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
public partial class MainWindow : StyleableWindow
|
||||
{
|
||||
internal static MainWindowViewModel MainWindowViewModel { get; private set; }
|
||||
private bool _canUpdate;
|
||||
private bool _isClosing;
|
||||
private bool _isLoading;
|
||||
|
||||
private Control _mainViewContent;
|
||||
private bool _isLoading;
|
||||
|
||||
private UserChannelPersistence _userChannelPersistence;
|
||||
private static bool _deferLoad;
|
||||
private static string _launchPath;
|
||||
private static bool _startFullscreen;
|
||||
private string _currentEmulatedGamePath;
|
||||
internal readonly AvaHostUiHandler UiHandler;
|
||||
private AutoResetEvent _rendererWaitEvent;
|
||||
|
||||
public VirtualFileSystem VirtualFileSystem { get; private set; }
|
||||
public ContentManager ContentManager { get; private set; }
|
||||
@ -58,30 +46,17 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
|
||||
public LibHacHorizonManager LibHacHorizonManager { get; private set; }
|
||||
|
||||
internal AppHost AppHost { get; private set; }
|
||||
public InputManager InputManager { get; private set; }
|
||||
|
||||
internal RendererHost RendererControl { get; private set; }
|
||||
internal MainWindowViewModel ViewModel { get; private set; }
|
||||
public SettingsWindow SettingsWindow { get; set; }
|
||||
|
||||
public bool CanUpdate
|
||||
{
|
||||
get => _canUpdate;
|
||||
set
|
||||
{
|
||||
_canUpdate = value;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() => UpdateMenuItem.IsEnabled = _canUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ShowKeyErrorOnLoad { get; set; }
|
||||
public ApplicationLibrary ApplicationLibrary { get; set; }
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
ViewModel = new MainWindowViewModel(this);
|
||||
ViewModel = new MainWindowViewModel();
|
||||
|
||||
MainWindowViewModel = ViewModel;
|
||||
|
||||
@ -92,10 +67,10 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
|
||||
UiHandler = new AvaHostUiHandler(this);
|
||||
|
||||
Title = $"Ryujinx {Program.Version}";
|
||||
ViewModel.Title = $"Ryujinx {Program.Version}";
|
||||
|
||||
// NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point.
|
||||
double barHeight = MenuBar.MinHeight + StatusBar.MinHeight;
|
||||
double barHeight = MenuBar.MinHeight + StatusBarView.StatusBar.MinHeight;
|
||||
Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight;
|
||||
Width /= Program.WindowScaleFactor;
|
||||
|
||||
@ -103,14 +78,37 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
Initialize();
|
||||
|
||||
ViewModel.Initialize();
|
||||
|
||||
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver());
|
||||
|
||||
ViewModel.Initialize(
|
||||
ContentManager,
|
||||
ApplicationLibrary,
|
||||
VirtualFileSystem,
|
||||
AccountManager,
|
||||
InputManager,
|
||||
_userChannelPersistence,
|
||||
LibHacHorizonManager,
|
||||
UiHandler,
|
||||
ShowLoading,
|
||||
SwitchToGameControl,
|
||||
SetMainContent,
|
||||
this);
|
||||
|
||||
ViewModel.RefreshFirmwareStatus();
|
||||
|
||||
LoadGameList();
|
||||
|
||||
this.GetObservable(IsActiveProperty).Subscribe(IsActiveChanged);
|
||||
}
|
||||
|
||||
_rendererWaitEvent = new AutoResetEvent(false);
|
||||
ApplicationLibrary.ApplicationCountUpdated += ApplicationLibrary_ApplicationCountUpdated;
|
||||
ApplicationLibrary.ApplicationAdded += ApplicationLibrary_ApplicationAdded;
|
||||
ViewModel.ReloadGameList += ReloadGameList;
|
||||
}
|
||||
|
||||
private void IsActiveChanged(bool obj)
|
||||
{
|
||||
ViewModel.IsActive = obj;
|
||||
}
|
||||
|
||||
public void LoadGameList()
|
||||
@ -122,45 +120,51 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
|
||||
_isLoading = true;
|
||||
|
||||
ViewModel.LoadApplications();
|
||||
LoadApplications();
|
||||
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
private void Update_StatusBar(object sender, StatusUpdatedEventArgs args)
|
||||
{
|
||||
if (ViewModel.ShowMenuAndStatusBar && !ViewModel.ShowLoadProgress)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (args.VSyncEnabled)
|
||||
{
|
||||
ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ff2eeac9"));
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel.VsyncColor = new SolidColorBrush(Color.Parse("#ffff4554"));
|
||||
}
|
||||
|
||||
ViewModel.DockedStatusText = args.DockedMode;
|
||||
ViewModel.AspectRatioStatusText = args.AspectRatio;
|
||||
ViewModel.GameStatusText = args.GameStatus;
|
||||
ViewModel.VolumeStatusText = args.VolumeStatus;
|
||||
ViewModel.FifoStatusText = args.FifoStatus;
|
||||
ViewModel.GpuNameText = args.GpuName;
|
||||
ViewModel.BackendText = args.GpuBackend;
|
||||
|
||||
ViewModel.ShowStatusSeparator = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void HandleScalingChanged(double scale)
|
||||
{
|
||||
Program.DesktopScaleFactor = scale;
|
||||
base.HandleScalingChanged(scale);
|
||||
}
|
||||
|
||||
public void AddApplication(ApplicationData applicationData)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
ViewModel.Applications.Add(applicationData);
|
||||
});
|
||||
}
|
||||
|
||||
private void ApplicationLibrary_ApplicationAdded(object sender, ApplicationAddedEventArgs e)
|
||||
{
|
||||
AddApplication(e.AppData);
|
||||
}
|
||||
|
||||
private void ApplicationLibrary_ApplicationCountUpdated(object sender, ApplicationCountUpdatedEventArgs e)
|
||||
{
|
||||
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, e.NumAppsLoaded, e.NumAppsFound);
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ViewModel.StatusBarProgressValue = e.NumAppsLoaded;
|
||||
ViewModel.StatusBarProgressMaximum = e.NumAppsFound;
|
||||
|
||||
if (e.NumAppsFound == 0)
|
||||
{
|
||||
StatusBarView.LoadProgressBar.IsVisible = false;
|
||||
}
|
||||
|
||||
if (e.NumAppsLoaded == e.NumAppsFound)
|
||||
{
|
||||
StatusBarView.LoadProgressBar.IsVisible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Application_Opened(object sender, ApplicationOpenedEventArgs args)
|
||||
{
|
||||
if (args.Application != null)
|
||||
@ -169,50 +173,12 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
|
||||
string path = new FileInfo(args.Application.Path).FullName;
|
||||
|
||||
LoadApplication(path);
|
||||
ViewModel.LoadApplication(path);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public async Task PerformanceCheck()
|
||||
{
|
||||
if (ConfigurationState.Instance.Logger.EnableTrace.Value)
|
||||
{
|
||||
string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledMessage];
|
||||
string secondaryMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckLoggingEnabledConfirmMessage];
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage,
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||
|
||||
if (result != UserResult.Yes)
|
||||
{
|
||||
ConfigurationState.Instance.Logger.EnableTrace.Value = false;
|
||||
|
||||
SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value))
|
||||
{
|
||||
string mainMessage = LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledMessage];
|
||||
string secondaryMessage =
|
||||
LocaleManager.Instance[LocaleKeys.DialogPerformanceCheckShaderDumpEnabledConfirmMessage];
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(mainMessage, secondaryMessage,
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||
|
||||
if (result != UserResult.Yes)
|
||||
{
|
||||
ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = "";
|
||||
|
||||
SaveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void DeferLoadApplication(string launchPathArg, bool startFullscreenArg)
|
||||
{
|
||||
_deferLoad = true;
|
||||
@ -220,109 +186,6 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
_startFullscreen = startFullscreenArg;
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998
|
||||
public async void LoadApplication(string path, bool startFullscreen = false, string titleName = "")
|
||||
#pragma warning restore CS1998
|
||||
{
|
||||
if (AppHost != null)
|
||||
{
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogLoadAppGameAlreadyLoadedSubMessage],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||
"",
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#if RELEASE
|
||||
await PerformanceCheck();
|
||||
#endif
|
||||
|
||||
Logger.RestartTime();
|
||||
|
||||
if (ViewModel.SelectedIcon == null)
|
||||
{
|
||||
ViewModel.SelectedIcon = ApplicationLibrary.GetApplicationIcon(path);
|
||||
}
|
||||
|
||||
PrepareLoadScreen();
|
||||
|
||||
_mainViewContent = MainContent.Content as Control;
|
||||
|
||||
RendererControl = new RendererHost(ConfigurationState.Instance.Logger.GraphicsDebugLevel);
|
||||
if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl)
|
||||
{
|
||||
RendererControl.CreateOpenGL();
|
||||
}
|
||||
else
|
||||
{
|
||||
RendererControl.CreateVulkan();
|
||||
}
|
||||
|
||||
AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this);
|
||||
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
if (!await AppHost.LoadGuestApplication())
|
||||
{
|
||||
AppHost.DisposeContext();
|
||||
AppHost = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CanUpdate = false;
|
||||
ViewModel.LoadHeading = string.IsNullOrWhiteSpace(titleName) ? string.Format(LocaleManager.Instance[LocaleKeys.LoadingHeading], AppHost.Device.Application.TitleName) : titleName;
|
||||
ViewModel.TitleName = string.IsNullOrWhiteSpace(titleName) ? AppHost.Device.Application.TitleName : titleName;
|
||||
|
||||
SwitchToGameControl(startFullscreen);
|
||||
|
||||
_currentEmulatedGamePath = path;
|
||||
|
||||
Thread gameThread = new(InitializeGame)
|
||||
{
|
||||
Name = "GUI.WindowThread"
|
||||
};
|
||||
gameThread.Start();
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeGame()
|
||||
{
|
||||
RendererControl.RendererInitialized += GlRenderer_Created;
|
||||
|
||||
AppHost.StatusUpdatedEvent += Update_StatusBar;
|
||||
AppHost.AppExit += AppHost_AppExit;
|
||||
|
||||
_rendererWaitEvent.WaitOne();
|
||||
|
||||
AppHost?.Start();
|
||||
|
||||
AppHost.DisposeContext();
|
||||
}
|
||||
|
||||
|
||||
private void HandleRelaunch()
|
||||
{
|
||||
if (_userChannelPersistence.PreviousIndex != -1 && _userChannelPersistence.ShouldRestart)
|
||||
{
|
||||
_userChannelPersistence.ShouldRestart = false;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
LoadApplication(_currentEmulatedGamePath);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise, clear state.
|
||||
_userChannelPersistence = new UserChannelPersistence();
|
||||
_currentEmulatedGamePath = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SwitchToGameControl(bool startFullscreen = false)
|
||||
{
|
||||
ViewModel.ShowLoadProgress = false;
|
||||
@ -331,14 +194,10 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
MainContent.Content = RendererControl;
|
||||
|
||||
if (startFullscreen && WindowState != WindowState.FullScreen)
|
||||
if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen)
|
||||
{
|
||||
ViewModel.ToggleFullscreen();
|
||||
}
|
||||
|
||||
RendererControl.Focus();
|
||||
});
|
||||
}
|
||||
|
||||
@ -350,71 +209,16 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (startFullscreen && WindowState != WindowState.FullScreen)
|
||||
if (startFullscreen && ViewModel.WindowState != WindowState.FullScreen)
|
||||
{
|
||||
ViewModel.ToggleFullscreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void GlRenderer_Created(object sender, EventArgs e)
|
||||
{
|
||||
ShowLoading();
|
||||
|
||||
_rendererWaitEvent.Set();
|
||||
}
|
||||
|
||||
private void AppHost_AppExit(object sender, EventArgs e)
|
||||
{
|
||||
if (_isClosing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.IsGameRunning = false;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
ViewModel.ShowMenuAndStatusBar = true;
|
||||
ViewModel.ShowContent = true;
|
||||
ViewModel.ShowLoadProgress = false;
|
||||
ViewModel.IsLoadingIndeterminate = false;
|
||||
CanUpdate = true;
|
||||
Cursor = Cursor.Default;
|
||||
|
||||
if (MainContent.Content != _mainViewContent)
|
||||
{
|
||||
MainContent.Content = _mainViewContent;
|
||||
}
|
||||
|
||||
AppHost = null;
|
||||
|
||||
HandleRelaunch();
|
||||
});
|
||||
|
||||
RendererControl.RendererInitialized -= GlRenderer_Created;
|
||||
RendererControl = null;
|
||||
|
||||
ViewModel.SelectedIcon = null;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
Title = $"Ryujinx {Program.Version}";
|
||||
});
|
||||
}
|
||||
|
||||
public void Sort_Checked(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is RadioButton button)
|
||||
{
|
||||
var sort = Enum.Parse<ApplicationSort>(button.Tag.ToString());
|
||||
ViewModel.Sort(sort);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void HandleWindowStateChanged(WindowState state)
|
||||
{
|
||||
WindowState = state;
|
||||
ViewModel.WindowState = state;
|
||||
|
||||
if (state != WindowState.Minimized)
|
||||
{
|
||||
@ -422,15 +226,6 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
}
|
||||
}
|
||||
|
||||
public void Order_Checked(object sender, RoutedEventArgs args)
|
||||
{
|
||||
if (sender is RadioButton button)
|
||||
{
|
||||
var tag = button.Tag.ToString();
|
||||
ViewModel.Sort(tag != "Descending");
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
_userChannelPersistence = new UserChannelPersistence();
|
||||
@ -457,8 +252,6 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
VirtualFileSystem.ReloadKeySet();
|
||||
|
||||
ApplicationHelper.Initialize(VirtualFileSystem, AccountManager, LibHacHorizonManager.RyujinxClient, this);
|
||||
|
||||
RefreshFirmwareStatus();
|
||||
}
|
||||
|
||||
protected void CheckLaunchState()
|
||||
@ -475,7 +268,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
_deferLoad = false;
|
||||
|
||||
LoadApplication(_launchPath, _startFullscreen);
|
||||
ViewModel.LoadApplication(_launchPath, _startFullscreen);
|
||||
}
|
||||
|
||||
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false, this))
|
||||
@ -487,35 +280,9 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshFirmwareStatus()
|
||||
{
|
||||
SystemVersion version = null;
|
||||
try
|
||||
{
|
||||
version = ContentManager.GetCurrentFirmwareVersion();
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
bool hasApplet = false;
|
||||
|
||||
if (version != null)
|
||||
{
|
||||
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion,
|
||||
version.VersionString);
|
||||
|
||||
hasApplet = version.Major > 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarSystemVersion, "0.0");
|
||||
}
|
||||
|
||||
ViewModel.IsAppletMenuActive = hasApplet;
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
VolumeStatus.Click += VolumeStatus_CheckedChanged;
|
||||
StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged;
|
||||
|
||||
GameGrid.ApplicationOpened += Application_Opened;
|
||||
|
||||
@ -535,6 +302,19 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
CheckLaunchState();
|
||||
}
|
||||
|
||||
private void SetMainContent(Control content = null)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
content = GameLibrary;
|
||||
}
|
||||
|
||||
if (MainContent.Content != content)
|
||||
{
|
||||
MainContent.Content = content;
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateGraphicsConfig()
|
||||
{
|
||||
GraphicsConfig.ResScale = ConfigurationState.Instance.Graphics.ResScale == -1 ? ConfigurationState.Instance.Graphics.ResScaleCustom : ConfigurationState.Instance.Graphics.ResScale;
|
||||
@ -553,99 +333,6 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
HotKeyManager.SetHotKey(ExitHotKey, new KeyGesture(Key.Escape));
|
||||
}
|
||||
|
||||
public static void SaveConfig()
|
||||
{
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
}
|
||||
|
||||
public void UpdateGameMetadata(string titleId)
|
||||
{
|
||||
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
|
||||
{
|
||||
if (DateTime.TryParse(appMetadata.LastPlayed, out DateTime lastPlayedDateTime))
|
||||
{
|
||||
double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
|
||||
|
||||
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void PrepareLoadScreen()
|
||||
{
|
||||
using MemoryStream stream = new MemoryStream(ViewModel.SelectedIcon);
|
||||
using var gameIconBmp = SixLabors.ImageSharp.Image.Load<Bgra32>(stream);
|
||||
|
||||
var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel<Bgra32>();
|
||||
|
||||
const int ColorDivisor = 4;
|
||||
|
||||
Color progressFgColor = Color.FromRgb(dominantColor.R, dominantColor.G, dominantColor.B);
|
||||
Color progressBgColor = Color.FromRgb(
|
||||
(byte)(dominantColor.R / ColorDivisor),
|
||||
(byte)(dominantColor.G / ColorDivisor),
|
||||
(byte)(dominantColor.B / ColorDivisor));
|
||||
|
||||
ViewModel.ProgressBarForegroundColor = new SolidColorBrush(progressFgColor);
|
||||
ViewModel.ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor);
|
||||
}
|
||||
|
||||
private void SearchBox_OnKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
ViewModel.SearchText = SearchBox.Text;
|
||||
}
|
||||
|
||||
private async void StopEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (AppHost != null)
|
||||
{
|
||||
await AppHost.ShowExitPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
private async void PauseEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
AppHost?.Pause();
|
||||
});
|
||||
}
|
||||
|
||||
private async void ResumeEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
AppHost?.Resume();
|
||||
});
|
||||
}
|
||||
|
||||
private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem)
|
||||
{
|
||||
ViewModel.IsAmiiboRequested = AppHost.Device.System.SearchingForAmiibo(out _);
|
||||
}
|
||||
}
|
||||
|
||||
private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
AppHost.Device.EnableDeviceVsync = !AppHost.Device.EnableDeviceVsync;
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {AppHost.Device.EnableDeviceVsync}");
|
||||
}
|
||||
|
||||
private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
|
||||
}
|
||||
|
||||
private void AspectRatioStatus_PointerReleased(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;
|
||||
|
||||
ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
|
||||
}
|
||||
|
||||
private void VolumeStatus_CheckedChanged(object sender, SplitButtonClickEventArgs e)
|
||||
{
|
||||
var volumeSplitButton = sender as ToggleSplitButton;
|
||||
@ -653,20 +340,20 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
if (!volumeSplitButton.IsChecked)
|
||||
{
|
||||
AppHost.Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
|
||||
ViewModel.AppHost.Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppHost.Device.SetVolume(0);
|
||||
ViewModel.AppHost.Device.SetVolume(0);
|
||||
}
|
||||
|
||||
ViewModel.Volume = AppHost.Device.GetVolume();
|
||||
ViewModel.Volume = ViewModel.AppHost.Device.GetVolume();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
if (!_isClosing && AppHost != null && ConfigurationState.Instance.ShowConfirmExit)
|
||||
if (!ViewModel.IsClosing && ViewModel.AppHost != null && ConfigurationState.Instance.ShowConfirmExit)
|
||||
{
|
||||
e.Cancel = true;
|
||||
|
||||
@ -675,14 +362,14 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
return;
|
||||
}
|
||||
|
||||
_isClosing = true;
|
||||
ViewModel.IsClosing = true;
|
||||
|
||||
if (AppHost != null)
|
||||
if (ViewModel.AppHost != null)
|
||||
{
|
||||
AppHost.AppExit -= AppHost_AppExit;
|
||||
AppHost.AppExit += (sender, e) =>
|
||||
ViewModel.AppHost.AppExit -= ViewModel.AppHost_AppExit;
|
||||
ViewModel.AppHost.AppExit += (sender, e) =>
|
||||
{
|
||||
AppHost = null;
|
||||
ViewModel.AppHost = null;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
@ -691,7 +378,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
Close();
|
||||
});
|
||||
};
|
||||
AppHost?.Stop();
|
||||
ViewModel.AppHost?.Stop();
|
||||
|
||||
e.Cancel = true;
|
||||
|
||||
@ -709,13 +396,43 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
_isClosing = await ContentDialogHelper.CreateExitDialog();
|
||||
ViewModel.IsClosing = await ContentDialogHelper.CreateExitDialog();
|
||||
|
||||
if (_isClosing)
|
||||
if (ViewModel.IsClosing)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async void LoadApplications()
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
ViewModel.Applications.Clear();
|
||||
|
||||
StatusBarView.LoadProgressBar.IsVisible = true;
|
||||
ViewModel.StatusBarProgressMaximum = 0;
|
||||
ViewModel.StatusBarProgressValue = 0;
|
||||
|
||||
LocaleManager.Instance.UpdateDynamicValue(LocaleKeys.StatusBarGamesLoaded, 0, 0);
|
||||
});
|
||||
|
||||
ReloadGameList();
|
||||
}
|
||||
|
||||
private void ReloadGameList()
|
||||
{
|
||||
if (_isLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
|
||||
ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs.Value, ConfigurationState.Instance.System.Language);
|
||||
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,9 +7,9 @@ namespace Ryujinx.Common.Logging
|
||||
AudioRenderer,
|
||||
Configuration,
|
||||
Cpu,
|
||||
Font,
|
||||
Emulation,
|
||||
FFmpeg,
|
||||
Font,
|
||||
Gpu,
|
||||
Hid,
|
||||
Host1x,
|
||||
@ -66,6 +66,7 @@ namespace Ryujinx.Common.Logging
|
||||
ServiceVi,
|
||||
SurfaceFlinger,
|
||||
TamperMachine,
|
||||
Ui,
|
||||
Vic
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ namespace Ryujinx.Common.Logging
|
||||
files[i].Delete();
|
||||
}
|
||||
|
||||
string version = ReleaseInformations.GetVersion();
|
||||
string version = ReleaseInformation.GetVersion();
|
||||
|
||||
// Get path for the current time
|
||||
path = Path.Combine(logDir.FullName, $"Ryujinx_{version}_{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.log");
|
||||
|
@ -5,7 +5,7 @@ using System.Reflection;
|
||||
namespace Ryujinx.Common
|
||||
{
|
||||
// DO NOT EDIT, filled by CI
|
||||
public static class ReleaseInformations
|
||||
public static class ReleaseInformation
|
||||
{
|
||||
private const string FlatHubChannelOwner = "flathub";
|
||||
|
@ -123,6 +123,8 @@ namespace Ryujinx.HLE.HOS
|
||||
|
||||
internal LibHacHorizonManager LibHacHorizonManager { get; private set; }
|
||||
|
||||
internal ServiceTable ServiceTable { get; private set; }
|
||||
|
||||
public bool IsPaused { get; private set; }
|
||||
|
||||
public Horizon(Switch device)
|
||||
@ -326,6 +328,7 @@ namespace Ryujinx.HLE.HOS
|
||||
|
||||
private void StartNewServices()
|
||||
{
|
||||
ServiceTable = new ServiceTable();
|
||||
var services = ServiceTable.GetServices(new HorizonOptions(Device.Configuration.IgnoreMissingServices));
|
||||
|
||||
foreach (var service in services)
|
||||
|
@ -182,6 +182,8 @@ namespace Ryujinx.HLE.HOS
|
||||
byte[] arguments = null,
|
||||
params IExecutable[] executables)
|
||||
{
|
||||
context.Device.System.ServiceTable.WaitServicesReady();
|
||||
|
||||
LibHac.Result rc = metaData.GetNpdm(out var npdm);
|
||||
|
||||
if (rc.IsFailure())
|
||||
|
@ -1,19 +0,0 @@
|
||||
using Ryujinx.HLE.HOS.Services.Lm.LogService;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Lm
|
||||
{
|
||||
[Service("lm")]
|
||||
class ILogService : IpcService
|
||||
{
|
||||
public ILogService(ServiceCtx context) { }
|
||||
|
||||
[CommandHipc(0)]
|
||||
// Initialize(u64, pid) -> object<nn::lm::ILogger>
|
||||
public ResultCode Initialize(ServiceCtx context)
|
||||
{
|
||||
MakeObject(context, new ILogger());
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Lm.LogService
|
||||
{
|
||||
class ILogger : IpcService
|
||||
{
|
||||
public ILogger() { }
|
||||
|
||||
[CommandHipc(0)]
|
||||
// Log(buffer<unknown, 0x21>)
|
||||
public ResultCode Log(ServiceCtx context)
|
||||
{
|
||||
Logger.Guest?.Print(LogClass.ServiceLm, LogImpl(context));
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
private string LogImpl(ServiceCtx context)
|
||||
{
|
||||
(ulong bufPos, ulong bufSize) = context.Request.GetBufferType0x21();
|
||||
|
||||
byte[] logBuffer = new byte[bufSize];
|
||||
|
||||
context.Memory.Read(bufPos, logBuffer);
|
||||
|
||||
using MemoryStream ms = new MemoryStream(logBuffer);
|
||||
|
||||
BinaryReader reader = new BinaryReader(ms);
|
||||
|
||||
long pid = reader.ReadInt64();
|
||||
long threadContext = reader.ReadInt64();
|
||||
short flags = reader.ReadInt16();
|
||||
byte level = reader.ReadByte();
|
||||
byte verbosity = reader.ReadByte();
|
||||
int payloadLength = reader.ReadInt32();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"Guest Log:\n Log level: {(LmLogLevel)level}");
|
||||
|
||||
while (ms.Position < ms.Length)
|
||||
{
|
||||
int type = ReadEncodedInt(reader);
|
||||
int size = ReadEncodedInt(reader);
|
||||
|
||||
LmLogField field = (LmLogField)type;
|
||||
|
||||
string fieldStr = string.Empty;
|
||||
|
||||
if (field == LmLogField.Start)
|
||||
{
|
||||
reader.ReadBytes(size);
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (field == LmLogField.Stop)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (field == LmLogField.Line)
|
||||
{
|
||||
fieldStr = $"{field}: {reader.ReadInt32()}";
|
||||
}
|
||||
else if (field == LmLogField.DropCount)
|
||||
{
|
||||
fieldStr = $"{field}: {reader.ReadInt64()}";
|
||||
}
|
||||
else if (field == LmLogField.Time)
|
||||
{
|
||||
fieldStr = $"{field}: {reader.ReadInt64()}s";
|
||||
}
|
||||
else if (field < LmLogField.Count)
|
||||
{
|
||||
fieldStr = $"{field}: '{Encoding.UTF8.GetString(reader.ReadBytes(size)).TrimEnd()}'";
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldStr = $"Field{field}: '{Encoding.UTF8.GetString(reader.ReadBytes(size)).TrimEnd()}'";
|
||||
}
|
||||
|
||||
sb.AppendLine($" {fieldStr}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static int ReadEncodedInt(BinaryReader reader)
|
||||
{
|
||||
int result = 0;
|
||||
int position = 0;
|
||||
|
||||
byte encoded;
|
||||
|
||||
do
|
||||
{
|
||||
encoded = reader.ReadByte();
|
||||
|
||||
result += (encoded & 0x7F) << (7 * position);
|
||||
|
||||
position++;
|
||||
|
||||
} while ((encoded & 0x80) != 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Lm.LogService
|
||||
{
|
||||
enum LmLogField
|
||||
{
|
||||
Start = 0,
|
||||
Stop = 1,
|
||||
Message = 2,
|
||||
Line = 3,
|
||||
Filename = 4,
|
||||
Function = 5,
|
||||
Module = 6,
|
||||
Thread = 7,
|
||||
DropCount = 8,
|
||||
Time = 9,
|
||||
ProgramName = 10,
|
||||
Count
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Lm.LogService
|
||||
{
|
||||
enum LmLogLevel
|
||||
{
|
||||
Trace,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Critical
|
||||
}
|
||||
}
|
@ -1,182 +0,0 @@
|
||||
using MsgPack;
|
||||
using MsgPack.Serialization;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.HLE.Utilities;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Prepo
|
||||
{
|
||||
[Service("prepo:a", PrepoServicePermissionLevel.Admin)] // 1.0.0-5.1.0
|
||||
[Service("prepo:a2", PrepoServicePermissionLevel.Admin)] // 6.0.0+
|
||||
[Service("prepo:m", PrepoServicePermissionLevel.Manager)]
|
||||
[Service("prepo:u", PrepoServicePermissionLevel.User)]
|
||||
[Service("prepo:s", PrepoServicePermissionLevel.System)]
|
||||
class IPrepoService : IpcService
|
||||
{
|
||||
private PrepoServicePermissionLevel _permission;
|
||||
private ulong _systemSessionId;
|
||||
|
||||
public IPrepoService(ServiceCtx context, PrepoServicePermissionLevel permission)
|
||||
{
|
||||
_permission = permission;
|
||||
}
|
||||
|
||||
[CommandHipc(10100)] // 1.0.0-5.1.0
|
||||
[CommandHipc(10102)] // 6.0.0-9.2.0
|
||||
[CommandHipc(10104)] // 10.0.0+
|
||||
// SaveReport(u64, pid, buffer<u8, 9>, buffer<bytes, 5>)
|
||||
public ResultCode SaveReport(ServiceCtx context)
|
||||
{
|
||||
if ((_permission & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return ResultCode.PermissionDenied;
|
||||
}
|
||||
|
||||
// We don't care about the differences since we don't use the play report.
|
||||
return ProcessReport(context, withUserID: false);
|
||||
}
|
||||
|
||||
[CommandHipc(10101)] // 1.0.0-5.1.0
|
||||
[CommandHipc(10103)] // 6.0.0-9.2.0
|
||||
[CommandHipc(10105)] // 10.0.0+
|
||||
// SaveReportWithUser(nn::account::Uid, u64, pid, buffer<u8, 9>, buffer<bytes, 5>)
|
||||
public ResultCode SaveReportWithUser(ServiceCtx context)
|
||||
{
|
||||
if ((_permission & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return ResultCode.PermissionDenied;
|
||||
}
|
||||
|
||||
// We don't care about the differences since we don't use the play report.
|
||||
return ProcessReport(context, withUserID: true);
|
||||
}
|
||||
|
||||
[CommandHipc(10200)]
|
||||
// RequestImmediateTransmission()
|
||||
public ResultCode RequestImmediateTransmission(ServiceCtx context)
|
||||
{
|
||||
// It signals an event of nn::prepo::detail::service::core::TransmissionStatusManager that requests the transmission of the report.
|
||||
// Since we don't use reports it's fine to do nothing.
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandHipc(10300)]
|
||||
// GetTransmissionStatus() -> u32
|
||||
public ResultCode GetTransmissionStatus(ServiceCtx context)
|
||||
{
|
||||
// It returns the transmission result of nn::prepo::detail::service::core::TransmissionStatusManager.
|
||||
// Since we don't use reports it's fine to return ResultCode.Success.
|
||||
context.ResponseData.Write((int)ResultCode.Success);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandHipc(10400)] // 9.0.0+
|
||||
// GetSystemSessionId() -> u64
|
||||
public ResultCode GetSystemSessionId(ServiceCtx context)
|
||||
{
|
||||
if ((_permission & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return ResultCode.PermissionDenied;
|
||||
}
|
||||
|
||||
if (_systemSessionId == 0)
|
||||
{
|
||||
byte[] randomBuffer = new byte[8];
|
||||
|
||||
Random.Shared.NextBytes(randomBuffer);
|
||||
|
||||
_systemSessionId = BitConverter.ToUInt64(randomBuffer, 0);
|
||||
}
|
||||
|
||||
context.ResponseData.Write(_systemSessionId);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandHipc(20100)]
|
||||
// SaveSystemReport(u64, pid, buffer<u8, 9>, buffer<bytes, 5>)
|
||||
public ResultCode SaveSystemReport(ServiceCtx context)
|
||||
{
|
||||
if ((_permission & PrepoServicePermissionLevel.System) != 0)
|
||||
{
|
||||
return ResultCode.PermissionDenied;
|
||||
}
|
||||
|
||||
// We don't care about the differences since we don't use the play report.
|
||||
return ProcessReport(context, withUserID: false);
|
||||
}
|
||||
|
||||
[CommandHipc(20101)]
|
||||
// SaveSystemReportWithUser(nn::account::Uid, u64, pid, buffer<u8, 9>, buffer<bytes, 5>)
|
||||
public ResultCode SaveSystemReportWithUser(ServiceCtx context)
|
||||
{
|
||||
if ((_permission & PrepoServicePermissionLevel.System) != 0)
|
||||
{
|
||||
return ResultCode.PermissionDenied;
|
||||
}
|
||||
|
||||
// We don't care about the differences since we don't use the play report.
|
||||
return ProcessReport(context, withUserID: true);
|
||||
}
|
||||
|
||||
private ResultCode ProcessReport(ServiceCtx context, bool withUserID)
|
||||
{
|
||||
UserId userId = withUserID ? context.RequestData.ReadStruct<UserId>() : new UserId();
|
||||
string gameRoom = StringUtils.ReadUtf8String(context);
|
||||
|
||||
if (withUserID)
|
||||
{
|
||||
if (userId.IsNull)
|
||||
{
|
||||
return ResultCode.InvalidArgument;
|
||||
}
|
||||
}
|
||||
|
||||
if (gameRoom == string.Empty)
|
||||
{
|
||||
return ResultCode.InvalidState;
|
||||
}
|
||||
|
||||
ulong inputPosition = context.Request.SendBuff[0].Position;
|
||||
ulong inputSize = context.Request.SendBuff[0].Size;
|
||||
|
||||
if (inputSize == 0)
|
||||
{
|
||||
return ResultCode.InvalidBufferSize;
|
||||
}
|
||||
|
||||
byte[] inputBuffer = new byte[inputSize];
|
||||
|
||||
context.Memory.Read(inputPosition, inputBuffer);
|
||||
|
||||
Logger.Info?.Print(LogClass.ServicePrepo, ReadReportBuffer(inputBuffer, gameRoom, userId));
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
private string ReadReportBuffer(byte[] buffer, string room, UserId userId)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(buffer);
|
||||
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("PlayReport log:");
|
||||
|
||||
if (!userId.IsNull)
|
||||
{
|
||||
builder.AppendLine($" UserId: {userId}");
|
||||
}
|
||||
|
||||
builder.AppendLine($" Room: {room}");
|
||||
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Prepo
|
||||
{
|
||||
enum ResultCode
|
||||
{
|
||||
ModuleId = 129,
|
||||
ErrorCodeShift = 9,
|
||||
|
||||
Success = 0,
|
||||
|
||||
InvalidArgument = (1 << ErrorCodeShift) | ModuleId,
|
||||
InvalidState = (5 << ErrorCodeShift) | ModuleId,
|
||||
InvalidBufferSize = (9 << ErrorCodeShift) | ModuleId,
|
||||
PermissionDenied = (90 << ErrorCodeShift) | ModuleId
|
||||
}
|
||||
}
|
@ -180,7 +180,7 @@ namespace Ryujinx.HLE.HOS.Services.Sm
|
||||
return ResultCode.InvalidName;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.ServiceSm, $"Register \"{name}\".");
|
||||
Logger.Debug?.Print(LogClass.ServiceSm, $"Register \"{name}\".");
|
||||
|
||||
KPort port = new KPort(context.Device.System.KernelContext, maxSessions, isLight, null);
|
||||
|
||||
|
9
Ryujinx.Headless.SDL2/HideCursor.cs
Normal file
9
Ryujinx.Headless.SDL2/HideCursor.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
public enum HideCursor
|
||||
{
|
||||
Never,
|
||||
OnIdle,
|
||||
Always
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.OpenGL;
|
||||
using Ryujinx.Input.HLE;
|
||||
using System;
|
||||
|
||||
using static SDL2.SDL;
|
||||
|
||||
namespace Ryujinx.Headless.SDL2.OpenGL
|
||||
@ -103,7 +102,13 @@ namespace Ryujinx.Headless.SDL2.OpenGL
|
||||
private GraphicsDebugLevel _glLogLevel;
|
||||
private SDL2OpenGLContext _openGLContext;
|
||||
|
||||
public OpenGLWindow(InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse) : base(inputManager, glLogLevel, aspectRatio, enableMouse)
|
||||
public OpenGLWindow(
|
||||
InputManager inputManager,
|
||||
GraphicsDebugLevel glLogLevel,
|
||||
AspectRatio aspectRatio,
|
||||
bool enableMouse,
|
||||
HideCursor hideCursor)
|
||||
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor)
|
||||
{
|
||||
_glLogLevel = glLogLevel;
|
||||
}
|
||||
|
@ -6,6 +6,14 @@ namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
public class Options
|
||||
{
|
||||
// General
|
||||
|
||||
[Option("root-data-dir", Required = false, HelpText = "Set the custom folder path for Ryujinx data.")]
|
||||
public string BaseDataDir { get; set; }
|
||||
|
||||
[Option("profile", Required = false, HelpText = "Set the user profile to launch the game with.")]
|
||||
public string UserProfile { get; set; }
|
||||
|
||||
// Input
|
||||
|
||||
[Option("input-profile-1", Required = false, HelpText = "Set the input profile in use for Player 1.")]
|
||||
@ -23,7 +31,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
[Option("input-profile-5", Required = false, HelpText = "Set the input profile in use for Player 5.")]
|
||||
public string InputProfile5Name { get; set; }
|
||||
|
||||
[Option("input-profile-6", Required = false, HelpText = "Set the input profile in use for Player 5.")]
|
||||
[Option("input-profile-6", Required = false, HelpText = "Set the input profile in use for Player 6.")]
|
||||
public string InputProfile6Name { get; set; }
|
||||
|
||||
[Option("input-profile-7", Required = false, HelpText = "Set the input profile in use for Player 7.")]
|
||||
@ -63,42 +71,45 @@ namespace Ryujinx.Headless.SDL2
|
||||
public string InputIdHandheld { get; set; }
|
||||
|
||||
[Option("enable-keyboard", Required = false, Default = false, HelpText = "Enable or disable keyboard support (Independent from controllers binding).")]
|
||||
public bool? EnableKeyboard { get; set; }
|
||||
public bool EnableKeyboard { get; set; }
|
||||
|
||||
[Option("enable-mouse", Required = false, Default = false, HelpText = "Enable or disable mouse support.")]
|
||||
public bool? EnableMouse { get; set; }
|
||||
public bool EnableMouse { get; set; }
|
||||
|
||||
[Option("hide-cursor", Required = false, Default = HideCursor.OnIdle, HelpText = "Change when the cursor gets hidden.")]
|
||||
public HideCursor HideCursor { get; set; }
|
||||
|
||||
[Option("list-input-profiles", Required = false, HelpText = "List inputs profiles.")]
|
||||
public bool? ListInputProfiles { get; set; }
|
||||
public bool ListInputProfiles { get; set; }
|
||||
|
||||
[Option("list-inputs-ids", Required = false, HelpText = "List inputs ids.")]
|
||||
public bool ListInputIds { get; set; }
|
||||
|
||||
// System
|
||||
|
||||
[Option("enable-ptc", Required = false, Default = true, HelpText = "Enables profiled translation cache persistency.")]
|
||||
public bool? EnablePtc { get; set; }
|
||||
[Option("disable-ptc", Required = false, HelpText = "Disables profiled persistent translation cache.")]
|
||||
public bool DisablePtc { get; set; }
|
||||
|
||||
[Option("enable-internet-connection", Required = false, Default = false, HelpText = "Enables guest Internet connection.")]
|
||||
public bool? EnableInternetAccess { get; set; }
|
||||
public bool EnableInternetAccess { get; set; }
|
||||
|
||||
[Option("enable-fs-integrity-checks", Required = false, Default = true, HelpText = "Enables integrity checks on Game content files.")]
|
||||
public bool? EnableFsIntegrityChecks { get; set; }
|
||||
[Option("disable-fs-integrity-checks", Required = false, HelpText = "Disables integrity checks on Game content files.")]
|
||||
public bool DisableFsIntegrityChecks { get; set; }
|
||||
|
||||
[Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")]
|
||||
public int FsGlobalAccessLogMode { get; set; }
|
||||
|
||||
[Option("enable-vsync", Required = false, Default = true, HelpText = "Enables Vertical Sync.")]
|
||||
public bool? EnableVsync { get; set; }
|
||||
[Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")]
|
||||
public bool DisableVsync { get; set; }
|
||||
|
||||
[Option("enable-shader-cache", Required = false, Default = true, HelpText = "Enables Shader cache.")]
|
||||
public bool? EnableShaderCache { get; set; }
|
||||
[Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")]
|
||||
public bool DisableShaderCache { get; set; }
|
||||
|
||||
[Option("enable-texture-recompression", Required = false, Default = false, HelpText = "Enables Texture recompression.")]
|
||||
public bool? EnableTextureRecompression { get; set; }
|
||||
public bool EnableTextureRecompression { get; set; }
|
||||
|
||||
[Option("enable-docked-mode", Required = false, Default = true, HelpText = "Enables Docked Mode.")]
|
||||
public bool? EnableDockedMode { get; set; }
|
||||
[Option("disable-docked-mode", Required = false, HelpText = "Disables Docked Mode.")]
|
||||
public bool DisableDockedMode { get; set; }
|
||||
|
||||
[Option("system-language", Required = false, Default = SystemLanguage.AmericanEnglish, HelpText = "Change System Language.")]
|
||||
public SystemLanguage SystemLanguage { get; set; }
|
||||
@ -120,32 +131,32 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
// Logging
|
||||
|
||||
[Option("enable-file-logging", Required = false, Default = false, HelpText = "Enables logging to a file on disk.")]
|
||||
public bool? EnableFileLog { get; set; }
|
||||
[Option("disable-file-logging", Required = false, Default = false, HelpText = "Disables logging to a file on disk.")]
|
||||
public bool DisableFileLog { get; set; }
|
||||
|
||||
[Option("enable-debug-logs", Required = false, Default = false, HelpText = "Enables printing debug log messages.")]
|
||||
public bool? LoggingEnableDebug { get; set; }
|
||||
public bool LoggingEnableDebug { get; set; }
|
||||
|
||||
[Option("enable-stub-logs", Required = false, Default = true, HelpText = "Enables printing stub log messages.")]
|
||||
public bool? LoggingEnableStub { get; set; }
|
||||
[Option("disable-stub-logs", Required = false, HelpText = "Disables printing stub log messages.")]
|
||||
public bool LoggingDisableStub { get; set; }
|
||||
|
||||
[Option("enable-info-logs", Required = false, Default = true, HelpText = "Enables printing info log messages.")]
|
||||
public bool? LoggingEnableInfo { get; set; }
|
||||
[Option("disable-info-logs", Required = false, HelpText = "Disables printing info log messages.")]
|
||||
public bool LoggingDisableInfo { get; set; }
|
||||
|
||||
[Option("enable-warning-logs", Required = false, Default = true, HelpText = "Enables printing warning log messages.")]
|
||||
public bool? LoggingEnableWarning { get; set; }
|
||||
[Option("disable-warning-logs", Required = false, HelpText = "Disables printing warning log messages.")]
|
||||
public bool LoggingDisableWarning { get; set; }
|
||||
|
||||
[Option("enable-error-logs", Required = false, Default = true, HelpText = "Enables printing error log messages.")]
|
||||
public bool? LoggingEnableError { get; set; }
|
||||
[Option("disable-error-logs", Required = false, HelpText = "Disables printing error log messages.")]
|
||||
public bool LoggingEnableError { get; set; }
|
||||
|
||||
[Option("enable-trace-logs", Required = false, Default = false, HelpText = "Enables printing trace log messages.")]
|
||||
public bool? LoggingEnableTrace { get; set; }
|
||||
public bool LoggingEnableTrace { get; set; }
|
||||
|
||||
[Option("enable-guest-logs", Required = false, Default = true, HelpText = "Enables printing guest log messages.")]
|
||||
public bool? LoggingEnableGuest { get; set; }
|
||||
[Option("disable-guest-logs", Required = false, HelpText = "Disables printing guest log messages.")]
|
||||
public bool LoggingDisableGuest { get; set; }
|
||||
|
||||
[Option("enable-fs-access-logs", Required = false, Default = false, HelpText = "Enables printing FS access log messages.")]
|
||||
public bool? LoggingEnableFsAccessLog { get; set; }
|
||||
public bool LoggingEnableFsAccessLog { get; set; }
|
||||
|
||||
[Option("graphics-debug-level", Required = false, Default = GraphicsDebugLevel.None, HelpText = "Change Graphics API debug log level.")]
|
||||
public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; }
|
||||
@ -164,6 +175,9 @@ namespace Ryujinx.Headless.SDL2
|
||||
[Option("backend-threading", Required = false, Default = BackendThreading.Auto, HelpText = "Whether or not backend threading is enabled. The \"Auto\" setting will determine whether threading should be enabled at runtime.")]
|
||||
public BackendThreading BackendThreading { get; set; }
|
||||
|
||||
[Option("disable-macro-hle", Required= false, HelpText = "Disables high-level emulation of Macro code. Leaving this enabled improves performance but may cause graphical glitches in some games.")]
|
||||
public bool DisableMacroHLE { get; set; }
|
||||
|
||||
[Option("graphics-shaders-dump-path", Required = false, HelpText = "Dumps shaders in this local directory. (Developer only)")]
|
||||
public string GraphicsShadersDumpPath { get; set; }
|
||||
|
||||
@ -176,10 +190,10 @@ namespace Ryujinx.Headless.SDL2
|
||||
// Hacks
|
||||
|
||||
[Option("expand-ram", Required = false, Default = false, HelpText = "Expands the RAM amount on the emulated system from 4GiB to 6GiB.")]
|
||||
public bool? ExpandRam { get; set; }
|
||||
public bool ExpandRam { get; set; }
|
||||
|
||||
[Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")]
|
||||
public bool? IgnoreMissingServices { get; set; }
|
||||
public bool IgnoreMissingServices { get; set; }
|
||||
|
||||
// Values
|
||||
|
||||
|
@ -33,7 +33,6 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
||||
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||
@ -59,24 +58,10 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Version = ReleaseInformations.GetVersion();
|
||||
Version = ReleaseInformation.GetVersion();
|
||||
|
||||
Console.Title = $"Ryujinx Console {Version} (Headless SDL2)";
|
||||
|
||||
AppDataManager.Initialize(null);
|
||||
|
||||
_virtualFileSystem = VirtualFileSystem.CreateInstance();
|
||||
_libHacHorizonManager = new LibHacHorizonManager();
|
||||
|
||||
_libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
|
||||
_libHacHorizonManager.InitializeArpServer();
|
||||
_libHacHorizonManager.InitializeBcatServer();
|
||||
_libHacHorizonManager.InitializeSystemClients();
|
||||
|
||||
_contentManager = new ContentManager(_virtualFileSystem);
|
||||
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient);
|
||||
_userChannelPersistence = new UserChannelPersistence();
|
||||
|
||||
if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
|
||||
{
|
||||
AutoResetEvent invoked = new AutoResetEvent(false);
|
||||
@ -97,15 +82,9 @@ namespace Ryujinx.Headless.SDL2
|
||||
};
|
||||
}
|
||||
|
||||
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
|
||||
|
||||
GraphicsConfig.EnableShaderCache = true;
|
||||
|
||||
Parser.Default.ParseArguments<Options>(args)
|
||||
.WithParsed(options => Load(options))
|
||||
.WithParsed(Load)
|
||||
.WithNotParsed(errors => errors.Output());
|
||||
|
||||
_inputManager.Dispose();
|
||||
}
|
||||
|
||||
private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
|
||||
@ -343,6 +322,24 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
static void Load(Options option)
|
||||
{
|
||||
AppDataManager.Initialize(option.BaseDataDir);
|
||||
|
||||
_virtualFileSystem = VirtualFileSystem.CreateInstance();
|
||||
_libHacHorizonManager = new LibHacHorizonManager();
|
||||
|
||||
_libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
|
||||
_libHacHorizonManager.InitializeArpServer();
|
||||
_libHacHorizonManager.InitializeBcatServer();
|
||||
_libHacHorizonManager.InitializeSystemClients();
|
||||
|
||||
_contentManager = new ContentManager(_virtualFileSystem);
|
||||
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
|
||||
_userChannelPersistence = new UserChannelPersistence();
|
||||
|
||||
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
|
||||
|
||||
GraphicsConfig.EnableShaderCache = true;
|
||||
|
||||
IGamepad gamepad;
|
||||
|
||||
if (option.ListInputIds)
|
||||
@ -378,8 +375,8 @@ namespace Ryujinx.Headless.SDL2
|
||||
}
|
||||
|
||||
_inputConfiguration = new List<InputConfig>();
|
||||
_enableKeyboard = (bool)option.EnableKeyboard;
|
||||
_enableMouse = (bool)option.EnableMouse;
|
||||
_enableKeyboard = option.EnableKeyboard;
|
||||
_enableMouse = option.EnableMouse;
|
||||
|
||||
void LoadPlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
|
||||
{
|
||||
@ -407,30 +404,31 @@ namespace Ryujinx.Headless.SDL2
|
||||
}
|
||||
|
||||
// Setup logging level
|
||||
Logger.SetEnable(LogLevel.Debug, (bool)option.LoggingEnableDebug);
|
||||
Logger.SetEnable(LogLevel.Stub, (bool)option.LoggingEnableStub);
|
||||
Logger.SetEnable(LogLevel.Info, (bool)option.LoggingEnableInfo);
|
||||
Logger.SetEnable(LogLevel.Warning, (bool)option.LoggingEnableWarning);
|
||||
Logger.SetEnable(LogLevel.Error, (bool)option.LoggingEnableError);
|
||||
Logger.SetEnable(LogLevel.Trace, (bool)option.LoggingEnableTrace);
|
||||
Logger.SetEnable(LogLevel.Guest, (bool)option.LoggingEnableGuest);
|
||||
Logger.SetEnable(LogLevel.AccessLog, (bool)option.LoggingEnableFsAccessLog);
|
||||
Logger.SetEnable(LogLevel.Debug, option.LoggingEnableDebug);
|
||||
Logger.SetEnable(LogLevel.Stub, !option.LoggingDisableStub);
|
||||
Logger.SetEnable(LogLevel.Info, !option.LoggingDisableInfo);
|
||||
Logger.SetEnable(LogLevel.Warning, !option.LoggingDisableWarning);
|
||||
Logger.SetEnable(LogLevel.Error, option.LoggingEnableError);
|
||||
Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace);
|
||||
Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest);
|
||||
Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog);
|
||||
|
||||
if ((bool)option.EnableFileLog)
|
||||
if (!option.DisableFileLog)
|
||||
{
|
||||
Logger.AddTarget(new AsyncLogTargetWrapper(
|
||||
new FileLogTarget(ReleaseInformations.GetBaseApplicationDirectory(), "file"),
|
||||
new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"),
|
||||
1000,
|
||||
AsyncLogTargetOverflowAction.Block
|
||||
));
|
||||
}
|
||||
|
||||
// Setup graphics configuration
|
||||
GraphicsConfig.EnableShaderCache = (bool)option.EnableShaderCache;
|
||||
GraphicsConfig.EnableTextureRecompression = (bool)option.EnableTextureRecompression;
|
||||
GraphicsConfig.EnableShaderCache = !option.DisableShaderCache;
|
||||
GraphicsConfig.EnableTextureRecompression = option.EnableTextureRecompression;
|
||||
GraphicsConfig.ResScale = option.ResScale;
|
||||
GraphicsConfig.MaxAnisotropy = option.MaxAnisotropy;
|
||||
GraphicsConfig.ShadersDumpPath = option.GraphicsShadersDumpPath;
|
||||
GraphicsConfig.EnableMacroHLE = !option.DisableMacroHLE;
|
||||
|
||||
while (true)
|
||||
{
|
||||
@ -443,6 +441,8 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
_userChannelPersistence.ShouldRestart = false;
|
||||
}
|
||||
|
||||
_inputManager.Dispose();
|
||||
}
|
||||
|
||||
private static void SetupProgressHandler()
|
||||
@ -479,8 +479,8 @@ namespace Ryujinx.Headless.SDL2
|
||||
private static WindowBase CreateWindow(Options options)
|
||||
{
|
||||
return options.GraphicsBackend == GraphicsBackend.Vulkan
|
||||
? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, (bool)options.EnableMouse)
|
||||
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, (bool)options.EnableMouse);
|
||||
? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor)
|
||||
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor);
|
||||
}
|
||||
|
||||
private static IRenderer CreateRenderer(Options options, WindowBase window)
|
||||
@ -533,20 +533,20 @@ namespace Ryujinx.Headless.SDL2
|
||||
_userChannelPersistence,
|
||||
renderer,
|
||||
new SDL2HardwareDeviceDriver(),
|
||||
(bool)options.ExpandRam ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB,
|
||||
options.ExpandRam ? MemoryConfiguration.MemoryConfiguration6GiB : MemoryConfiguration.MemoryConfiguration4GiB,
|
||||
window,
|
||||
options.SystemLanguage,
|
||||
options.SystemRegion,
|
||||
(bool)options.EnableVsync,
|
||||
(bool)options.EnableDockedMode,
|
||||
(bool)options.EnablePtc,
|
||||
(bool)options.EnableInternetAccess,
|
||||
(bool)options.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
|
||||
!options.DisableVsync,
|
||||
!options.DisableDockedMode,
|
||||
!options.DisablePtc,
|
||||
options.EnableInternetAccess,
|
||||
!options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
|
||||
options.FsGlobalAccessLogMode,
|
||||
options.SystemTimeOffset,
|
||||
options.SystemTimeZone,
|
||||
options.MemoryManagerMode,
|
||||
(bool)options.IgnoreMissingServices,
|
||||
options.IgnoreMissingServices,
|
||||
options.AspectRatio,
|
||||
options.AudioVolume);
|
||||
|
||||
@ -649,7 +649,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
|
||||
Logger.Warning?.Print(LogClass.Application, $"Couldn't load '{options.InputPath}'. Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
|
||||
|
||||
_emulationContext.Dispose();
|
||||
|
||||
|
@ -42,6 +42,10 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Ryujinx.bmp" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Due to .net core 3.1 embedded resource loading -->
|
||||
<PropertyGroup>
|
||||
<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
|
||||
|
BIN
Ryujinx.Headless.SDL2/Ryujinx.bmp
Normal file
BIN
Ryujinx.Headless.SDL2/Ryujinx.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
@ -10,7 +10,12 @@ namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
class SDL2MouseDriver : IGamepadDriver
|
||||
{
|
||||
private const int CursorHideIdleTime = 8; // seconds
|
||||
|
||||
private bool _isDisposed;
|
||||
private HideCursor _hideCursor;
|
||||
private bool _isHidden;
|
||||
private long _lastCursorMoveTime;
|
||||
|
||||
public bool[] PressedButtons { get; }
|
||||
|
||||
@ -18,9 +23,16 @@ namespace Ryujinx.Headless.SDL2
|
||||
public Vector2 Scroll { get; private set; }
|
||||
public Size _clientSize;
|
||||
|
||||
public SDL2MouseDriver()
|
||||
public SDL2MouseDriver(HideCursor hideCursor)
|
||||
{
|
||||
PressedButtons = new bool[(int)MouseButton.Count];
|
||||
_hideCursor = hideCursor;
|
||||
|
||||
if (_hideCursor == HideCursor.Always)
|
||||
{
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
_isHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@ -31,10 +43,53 @@ namespace Ryujinx.Headless.SDL2
|
||||
return (MouseButton)(rawButton - 1);
|
||||
}
|
||||
|
||||
public void UpdatePosition()
|
||||
{
|
||||
SDL_GetMouseState(out int posX, out int posY);
|
||||
Vector2 position = new(posX, posY);
|
||||
|
||||
if (CurrentPosition != position)
|
||||
{
|
||||
CurrentPosition = position;
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
CheckIdle();
|
||||
}
|
||||
|
||||
private void CheckIdle()
|
||||
{
|
||||
if (_hideCursor != HideCursor.OnIdle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
|
||||
|
||||
if (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency)
|
||||
{
|
||||
if (!_isHidden)
|
||||
{
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
_isHidden = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_isHidden)
|
||||
{
|
||||
SDL_ShowCursor(SDL_ENABLE);
|
||||
_isHidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(SDL_Event evnt)
|
||||
{
|
||||
if (evnt.type == SDL_EventType.SDL_MOUSEBUTTONDOWN || evnt.type == SDL_EventType.SDL_MOUSEBUTTONUP)
|
||||
switch (evnt.type)
|
||||
{
|
||||
case SDL_EventType.SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_EventType.SDL_MOUSEBUTTONUP:
|
||||
uint rawButton = evnt.button.button;
|
||||
|
||||
if (rawButton > 0 && rawButton <= (int)MouseButton.Count)
|
||||
@ -43,14 +98,20 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
CurrentPosition = new Vector2(evnt.button.x, evnt.button.y);
|
||||
}
|
||||
}
|
||||
else if (evnt.type == SDL_EventType.SDL_MOUSEMOTION)
|
||||
{
|
||||
|
||||
break;
|
||||
|
||||
// NOTE: On Linux using Wayland mouse motion events won't be received at all.
|
||||
case SDL_EventType.SDL_MOUSEMOTION:
|
||||
CurrentPosition = new Vector2(evnt.motion.x, evnt.motion.y);
|
||||
}
|
||||
else if (evnt.type == SDL_EventType.SDL_MOUSEWHEEL)
|
||||
{
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
|
||||
break;
|
||||
|
||||
case SDL_EventType.SDL_MOUSEWHEEL:
|
||||
Scroll = new Vector2(evnt.wheel.x, evnt.wheel.y);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,13 @@ namespace Ryujinx.Headless.SDL2.Vulkan
|
||||
{
|
||||
private GraphicsDebugLevel _glLogLevel;
|
||||
|
||||
public VulkanWindow(InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse) : base(inputManager, glLogLevel, aspectRatio, enableMouse)
|
||||
public VulkanWindow(
|
||||
InputManager inputManager,
|
||||
GraphicsDebugLevel glLogLevel,
|
||||
AspectRatio aspectRatio,
|
||||
bool enableMouse,
|
||||
HideCursor hideCursor)
|
||||
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor)
|
||||
{
|
||||
_glLogLevel = glLogLevel;
|
||||
}
|
||||
|
@ -14,13 +14,16 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using static SDL2.SDL;
|
||||
using Switch = Ryujinx.HLE.Switch;
|
||||
|
||||
namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
abstract class WindowBase : IHostUiHandler, IDisposable
|
||||
abstract partial class WindowBase : IHostUiHandler, IDisposable
|
||||
{
|
||||
protected const int DefaultWidth = 1280;
|
||||
protected const int DefaultHeight = 720;
|
||||
@ -29,6 +32,10 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
private static ConcurrentQueue<Action> MainThreadActions = new ConcurrentQueue<Action>();
|
||||
|
||||
[LibraryImport("SDL2")]
|
||||
// TODO: Remove this as soon as SDL2-CS was updated to expose this method publicly
|
||||
private static partial IntPtr SDL_LoadBMP_RW(IntPtr src, int freesrc);
|
||||
|
||||
public static void QueueMainThreadAction(Action action)
|
||||
{
|
||||
MainThreadActions.Enqueue(action);
|
||||
@ -66,9 +73,14 @@ namespace Ryujinx.Headless.SDL2
|
||||
private AspectRatio _aspectRatio;
|
||||
private bool _enableMouse;
|
||||
|
||||
public WindowBase(InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse)
|
||||
public WindowBase(
|
||||
InputManager inputManager,
|
||||
GraphicsDebugLevel glLogLevel,
|
||||
AspectRatio aspectRatio,
|
||||
bool enableMouse,
|
||||
HideCursor hideCursor)
|
||||
{
|
||||
MouseDriver = new SDL2MouseDriver();
|
||||
MouseDriver = new SDL2MouseDriver(hideCursor);
|
||||
_inputManager = inputManager;
|
||||
_inputManager.SetMouseDriver(MouseDriver);
|
||||
NpadManager = _inputManager.CreateNpadManager();
|
||||
@ -103,6 +115,34 @@ namespace Ryujinx.Headless.SDL2
|
||||
TouchScreenManager.Initialize(device);
|
||||
}
|
||||
|
||||
private void SetWindowIcon()
|
||||
{
|
||||
Stream iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.Headless.SDL2.Ryujinx.bmp");
|
||||
byte[] iconBytes = new byte[iconStream!.Length];
|
||||
|
||||
if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, "Failed to read icon to byte array.");
|
||||
iconStream.Close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
iconStream.Close();
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* iconPtr = iconBytes)
|
||||
{
|
||||
IntPtr rwOpsStruct = SDL_RWFromConstMem((IntPtr)iconPtr, iconBytes.Length);
|
||||
IntPtr iconHandle = SDL_LoadBMP_RW(rwOpsStruct, 1);
|
||||
|
||||
SDL_SetWindowIcon(WindowHandle, iconHandle);
|
||||
SDL_FreeSurface(iconHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeWindow()
|
||||
{
|
||||
string titleNameSection = string.IsNullOrWhiteSpace(Device.Application.TitleName) ? string.Empty
|
||||
@ -127,6 +167,8 @@ namespace Ryujinx.Headless.SDL2
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
SetWindowIcon();
|
||||
|
||||
_windowId = SDL_GetWindowID(WindowHandle);
|
||||
SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
|
||||
|
||||
@ -146,9 +188,11 @@ namespace Ryujinx.Headless.SDL2
|
||||
Renderer?.Window.SetSize(Width, Height);
|
||||
MouseDriver.SetClientSize(Width, Height);
|
||||
break;
|
||||
|
||||
case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
|
||||
Exit();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -331,6 +375,9 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
Device.Hid.DebugPad.Update();
|
||||
|
||||
// TODO: Replace this with MouseDriver.CheckIdle() when mouse motion events are received on every supported platform.
|
||||
MouseDriver.UpdatePosition();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -100,14 +100,6 @@ namespace Ryujinx.Horizon.Common
|
||||
}
|
||||
}
|
||||
|
||||
public void AbortOnFailureUnless(Result result, Result result2)
|
||||
{
|
||||
if (this != Success && this != result && this != result2)
|
||||
{
|
||||
ThrowInvalidResult();
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowInvalidResult()
|
||||
{
|
||||
throw new InvalidResultException(this);
|
||||
|
@ -123,6 +123,12 @@ namespace Ryujinx.Horizon.Generators.Hipc
|
||||
{
|
||||
string[] args = new string[method.ParameterList.Parameters.Count];
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, Array.Empty<CommandArg>()) }},");
|
||||
}
|
||||
else
|
||||
{
|
||||
int index = 0;
|
||||
|
||||
foreach (var parameter in method.ParameterList.Parameters)
|
||||
@ -163,6 +169,7 @@ namespace Ryujinx.Horizon.Generators.Hipc
|
||||
generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, {string.Join(", ", args)}) }},");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generator.LeaveScope(";");
|
||||
generator.LeaveScope();
|
||||
|
@ -3,10 +3,12 @@ namespace Ryujinx.Horizon
|
||||
public struct HorizonOptions
|
||||
{
|
||||
public bool IgnoreMissingServices { get; }
|
||||
public bool ThrowOnInvalidCommandIds { get; }
|
||||
|
||||
public HorizonOptions(bool ignoreMissingServices)
|
||||
{
|
||||
IgnoreMissingServices = ignoreMissingServices;
|
||||
ThrowOnInvalidCommandIds = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,6 @@
|
||||
{
|
||||
interface IService
|
||||
{
|
||||
abstract static void Main();
|
||||
abstract static void Main(ServiceTable serviceTable);
|
||||
}
|
||||
}
|
||||
|
@ -9,23 +9,23 @@ using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Horizon.LogManager
|
||||
namespace Ryujinx.Horizon.LogManager.Ipc
|
||||
{
|
||||
partial class LmLogger : IServiceObject
|
||||
partial class LmLogger : ILmLogger
|
||||
{
|
||||
private readonly LmLog _log;
|
||||
private readonly ulong _clientProcessId;
|
||||
private readonly LogService _log;
|
||||
private readonly ulong _pid;
|
||||
|
||||
public LmLogger(LmLog log, ulong clientProcessId)
|
||||
public LmLogger(LogService log, ulong pid)
|
||||
{
|
||||
_log = log;
|
||||
_clientProcessId = clientProcessId;
|
||||
_pid = pid;
|
||||
}
|
||||
|
||||
[CmifCommand(0)]
|
||||
public Result Log([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] Span<byte> message)
|
||||
{
|
||||
if (!SetProcessId(message, _clientProcessId))
|
||||
if (!SetProcessId(message, _pid))
|
||||
{
|
||||
return Result.Success;
|
||||
}
|
||||
@ -35,7 +35,7 @@ namespace Ryujinx.Horizon.LogManager
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(1)]
|
||||
[CmifCommand(1)] // 3.0.0+
|
||||
public Result SetDestination(LogDestination destination)
|
||||
{
|
||||
_log.LogDestination = destination;
|
||||
@ -48,7 +48,6 @@ namespace Ryujinx.Horizon.LogManager
|
||||
ref LogPacketHeader header = ref MemoryMarshal.Cast<byte, LogPacketHeader>(message)[0];
|
||||
|
||||
uint expectedMessageSize = (uint)Unsafe.SizeOf<LogPacketHeader>() + header.PayloadSize;
|
||||
|
||||
if (expectedMessageSize != (uint)message.Length)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceLm, $"Invalid message size (expected 0x{expectedMessageSize:X} but got 0x{message.Length:X}).");
|
||||
@ -63,13 +62,11 @@ namespace Ryujinx.Horizon.LogManager
|
||||
|
||||
private static string LogImpl(ReadOnlySpan<byte> message)
|
||||
{
|
||||
SpanReader reader = new SpanReader(message);
|
||||
|
||||
SpanReader reader = new(message);
|
||||
LogPacketHeader header = reader.Read<LogPacketHeader>();
|
||||
StringBuilder builder = new();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"Guest Log:\n Log level: {header.Severity}");
|
||||
builder.AppendLine($"Guest Log:\n Log level: {header.Severity}");
|
||||
|
||||
while (reader.Length > 0)
|
||||
{
|
||||
@ -78,7 +75,7 @@ namespace Ryujinx.Horizon.LogManager
|
||||
|
||||
LogDataChunkKey field = (LogDataChunkKey)type;
|
||||
|
||||
string fieldStr = string.Empty;
|
||||
string fieldStr;
|
||||
|
||||
if (field == LogDataChunkKey.Start)
|
||||
{
|
||||
@ -111,10 +108,10 @@ namespace Ryujinx.Horizon.LogManager
|
||||
fieldStr = $"Field{field}: '{Encoding.UTF8.GetString(reader.GetSpan(size)).TrimEnd()}'";
|
||||
}
|
||||
|
||||
sb.AppendLine($" {fieldStr}");
|
||||
builder.AppendLine($" {fieldStr}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static int ReadUleb128(ref SpanReader reader)
|
@ -2,16 +2,17 @@
|
||||
using Ryujinx.Horizon.Sdk.Lm;
|
||||
using Ryujinx.Horizon.Sdk.Sf;
|
||||
|
||||
namespace Ryujinx.Horizon.LogManager
|
||||
namespace Ryujinx.Horizon.LogManager.Ipc
|
||||
{
|
||||
partial class LmLog : IServiceObject
|
||||
partial class LogService : ILogService
|
||||
{
|
||||
public LogDestination LogDestination { get; set; } = LogDestination.TargetManager;
|
||||
|
||||
[CmifCommand(0)]
|
||||
public Result OpenLogger(out LmLogger logger, [ClientProcessId] ulong clientProcessId)
|
||||
public Result OpenLogger(out LmLogger logger, [ClientProcessId] ulong pid)
|
||||
{
|
||||
logger = new LmLogger(this, clientProcessId);
|
||||
// NOTE: Internal name is Logger, but we rename it LmLogger to avoid name clash with Ryujinx.Common.Logging logger.
|
||||
logger = new LmLogger(this, pid);
|
||||
|
||||
return Result.Success;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
||||
using Ryujinx.Horizon.LogManager.Ipc;
|
||||
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
||||
using Ryujinx.Horizon.Sdk.Sm;
|
||||
using Ryujinx.Horizon.Sm;
|
||||
|
||||
namespace Ryujinx.Horizon.LogManager
|
||||
{
|
||||
@ -11,34 +11,23 @@ namespace Ryujinx.Horizon.LogManager
|
||||
private const int PointerBufferSize = 0x400;
|
||||
private const int MaxDomains = 31;
|
||||
private const int MaxDomainObjects = 61;
|
||||
|
||||
private const int MaxPortsCount = 1;
|
||||
|
||||
private static readonly ManagerOptions _logManagerOptions = new ManagerOptions(
|
||||
PointerBufferSize,
|
||||
MaxDomains,
|
||||
MaxDomainObjects,
|
||||
false);
|
||||
|
||||
private static readonly ServiceName _logServiceName = ServiceName.Encode("lm");
|
||||
private static readonly ManagerOptions _logManagerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
|
||||
|
||||
private SmApi _sm;
|
||||
private ServerManager _serverManager;
|
||||
|
||||
private LmLog _logServiceObject;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
HeapAllocator allocator = new HeapAllocator();
|
||||
HeapAllocator allocator = new();
|
||||
|
||||
_sm = new SmApi();
|
||||
_sm.Initialize().AbortOnFailure();
|
||||
|
||||
_serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _logManagerOptions, LogMaxSessionsCount);
|
||||
|
||||
_logServiceObject = new LmLog();
|
||||
|
||||
_serverManager.RegisterObjectForServer(_logServiceObject, _logServiceName, LogMaxSessionsCount);
|
||||
_serverManager.RegisterObjectForServer(new LogService(), ServiceName.Encode("lm"), LogMaxSessionsCount);
|
||||
}
|
||||
|
||||
public void ServiceRequests()
|
||||
|
@ -2,11 +2,14 @@
|
||||
{
|
||||
class LmMain : IService
|
||||
{
|
||||
public static void Main()
|
||||
public static void Main(ServiceTable serviceTable)
|
||||
{
|
||||
LmIpcServer ipcServer = new LmIpcServer();
|
||||
LmIpcServer ipcServer = new();
|
||||
|
||||
ipcServer.Initialize();
|
||||
|
||||
serviceTable.SignalServiceReady();
|
||||
|
||||
ipcServer.ServiceRequests();
|
||||
ipcServer.Shutdown();
|
||||
}
|
||||
|
218
Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs
Normal file
218
Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs
Normal file
@ -0,0 +1,218 @@
|
||||
using MsgPack;
|
||||
using MsgPack.Serialization;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Horizon.Prepo.Types;
|
||||
using Ryujinx.Horizon.Sdk.Account;
|
||||
using Ryujinx.Horizon.Sdk.Prepo;
|
||||
using Ryujinx.Horizon.Sdk.Sf;
|
||||
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Horizon.Prepo.Ipc
|
||||
{
|
||||
partial class PrepoService : IPrepoService
|
||||
{
|
||||
enum PlayReportKind
|
||||
{
|
||||
Normal,
|
||||
System
|
||||
}
|
||||
|
||||
private readonly PrepoServicePermissionLevel _permissionLevel;
|
||||
private ulong _systemSessionId;
|
||||
|
||||
private bool _immediateTransmissionEnabled = false;
|
||||
private bool _userAgreementCheckEnabled = true;
|
||||
|
||||
public PrepoService(PrepoServicePermissionLevel permissionLevel)
|
||||
{
|
||||
_permissionLevel = permissionLevel;
|
||||
}
|
||||
|
||||
[CmifCommand(10100)] // 1.0.0-5.1.0
|
||||
[CmifCommand(10102)] // 6.0.0-9.2.0
|
||||
[CmifCommand(10104)] // 10.0.0+
|
||||
public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
ProcessPlayReport(PlayReportKind.Normal, pid, gameRoomBuffer, reportBuffer, Uid.Null);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(10101)] // 1.0.0-5.1.0
|
||||
[CmifCommand(10103)] // 6.0.0-9.2.0
|
||||
[CmifCommand(10105)] // 10.0.0+
|
||||
public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
ProcessPlayReport(PlayReportKind.Normal, pid, gameRoomBuffer, reportBuffer, userId, true);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(10200)]
|
||||
public Result RequestImmediateTransmission()
|
||||
{
|
||||
_immediateTransmissionEnabled = true;
|
||||
|
||||
// It signals an event of nn::prepo::detail::service::core::TransmissionStatusManager that requests the transmission of the report.
|
||||
// Since we don't use reports, it's fine to do nothing.
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(10300)]
|
||||
public Result GetTransmissionStatus(out int status)
|
||||
{
|
||||
status = 0;
|
||||
|
||||
if (_immediateTransmissionEnabled && _userAgreementCheckEnabled)
|
||||
{
|
||||
status = 1;
|
||||
}
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(10400)] // 9.0.0+
|
||||
public Result GetSystemSessionId(out ulong systemSessionId)
|
||||
{
|
||||
systemSessionId = default;
|
||||
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
if (_systemSessionId == 0)
|
||||
{
|
||||
_systemSessionId = (ulong)Random.Shared.NextInt64();
|
||||
}
|
||||
|
||||
systemSessionId = _systemSessionId;
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(20100)]
|
||||
public Result SaveSystemReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.System) != 0)
|
||||
{
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
return ProcessPlayReport(PlayReportKind.System, pid, gameRoomBuffer, reportBuffer, Uid.Null);
|
||||
}
|
||||
|
||||
[CmifCommand(20101)]
|
||||
public Result SaveSystemReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.System) != 0)
|
||||
{
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
return ProcessPlayReport(PlayReportKind.System, pid, gameRoomBuffer, reportBuffer, userId, true);
|
||||
}
|
||||
|
||||
[CmifCommand(40100)] // 2.0.0+
|
||||
public Result IsUserAgreementCheckEnabled(out bool enabled)
|
||||
{
|
||||
enabled = false;
|
||||
|
||||
if (_permissionLevel == PrepoServicePermissionLevel.User || _permissionLevel == PrepoServicePermissionLevel.System)
|
||||
{
|
||||
enabled = _userAgreementCheckEnabled;
|
||||
|
||||
// If "enabled" is false, it sets some internal fields to 0.
|
||||
// Then, it mounts "prepo-sys:/is_user_agreement_check_enabled.bin" and returns the contained bool.
|
||||
// We can return the private bool instead, we don't care about the agreement since we don't send reports.
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
[CmifCommand(40101)] // 2.0.0+
|
||||
public Result SetUserAgreementCheckEnabled(bool enabled)
|
||||
{
|
||||
if (_permissionLevel == PrepoServicePermissionLevel.User || _permissionLevel == PrepoServicePermissionLevel.System)
|
||||
{
|
||||
_userAgreementCheckEnabled = enabled;
|
||||
|
||||
// If "enabled" is false, it sets some internal fields to 0.
|
||||
// Then, it mounts "prepo-sys:/is_user_agreement_check_enabled.bin" and stores the "enabled" value.
|
||||
// We can store in the private bool instead, we don't care about the agreement since we don't send reports.
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
private static Result ProcessPlayReport(PlayReportKind playReportKind, ulong pid, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, Uid userId, bool withUserId = false)
|
||||
{
|
||||
if (withUserId)
|
||||
{
|
||||
if (userId.IsNull)
|
||||
{
|
||||
return PrepoResult.InvalidArgument;
|
||||
}
|
||||
}
|
||||
|
||||
if (gameRoomBuffer.Length > 31)
|
||||
{
|
||||
return PrepoResult.InvalidArgument;
|
||||
}
|
||||
|
||||
string gameRoom = Encoding.UTF8.GetString(gameRoomBuffer).TrimEnd();
|
||||
|
||||
if (gameRoom == string.Empty)
|
||||
{
|
||||
return PrepoResult.InvalidState;
|
||||
}
|
||||
|
||||
if (reportBuffer.Length == 0)
|
||||
{
|
||||
return PrepoResult.InvalidBufferSize;
|
||||
}
|
||||
|
||||
// NOTE: The service calls arp:r using the pid to get the application id, if it fails PrepoResult.InvalidPid is returned.
|
||||
// Reports are stored internally and an event is signaled to transmit them.
|
||||
|
||||
StringBuilder builder = new();
|
||||
MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(reportBuffer.ToArray());
|
||||
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("PlayReport log:");
|
||||
builder.AppendLine($" Kind: {playReportKind}");
|
||||
builder.AppendLine($" Pid: {pid}");
|
||||
|
||||
if (!userId.IsNull)
|
||||
{
|
||||
builder.AppendLine($" UserId: {userId}");
|
||||
}
|
||||
|
||||
builder.AppendLine($" Room: {gameRoom}");
|
||||
builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}");
|
||||
|
||||
Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString());
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
49
Ryujinx.Horizon/Prepo/PrepoIpcServer.cs
Normal file
49
Ryujinx.Horizon/Prepo/PrepoIpcServer.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using Ryujinx.Horizon.Prepo.Types;
|
||||
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
||||
using Ryujinx.Horizon.Sdk.Sm;
|
||||
|
||||
namespace Ryujinx.Horizon.Prepo
|
||||
{
|
||||
class PrepoIpcServer
|
||||
{
|
||||
private const int PrepoMaxSessionsCount = 12;
|
||||
private const int PrepoTotalMaxSessionsCount = PrepoMaxSessionsCount * 6;
|
||||
|
||||
private const int PointerBufferSize = 0x3800;
|
||||
private const int MaxDomains = 64;
|
||||
private const int MaxDomainObjects = 16;
|
||||
private const int MaxPortsCount = 6;
|
||||
|
||||
private static readonly ManagerOptions _logManagerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
|
||||
|
||||
private SmApi _sm;
|
||||
private PrepoServerManager _serverManager;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
HeapAllocator allocator = new();
|
||||
|
||||
_sm = new SmApi();
|
||||
_sm.Initialize().AbortOnFailure();
|
||||
|
||||
_serverManager = new PrepoServerManager(allocator, _sm, MaxPortsCount, _logManagerOptions, PrepoTotalMaxSessionsCount);
|
||||
|
||||
_serverManager.RegisterServer((int)PrepoPortIndex.Admin, ServiceName.Encode("prepo:a"), PrepoMaxSessionsCount); // 1.0.0-5.1.0
|
||||
_serverManager.RegisterServer((int)PrepoPortIndex.Admin2, ServiceName.Encode("prepo:a2"), PrepoMaxSessionsCount); // 6.0.0+
|
||||
_serverManager.RegisterServer((int)PrepoPortIndex.Manager, ServiceName.Encode("prepo:m"), PrepoMaxSessionsCount);
|
||||
_serverManager.RegisterServer((int)PrepoPortIndex.User, ServiceName.Encode("prepo:u"), PrepoMaxSessionsCount);
|
||||
_serverManager.RegisterServer((int)PrepoPortIndex.System, ServiceName.Encode("prepo:s"), PrepoMaxSessionsCount);
|
||||
_serverManager.RegisterServer((int)PrepoPortIndex.Debug, ServiceName.Encode("prepo:d"), PrepoMaxSessionsCount); // 1.0.0
|
||||
}
|
||||
|
||||
public void ServiceRequests()
|
||||
{
|
||||
_serverManager.ServiceRequests();
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_serverManager.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
17
Ryujinx.Horizon/Prepo/PrepoMain.cs
Normal file
17
Ryujinx.Horizon/Prepo/PrepoMain.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Ryujinx.Horizon.Prepo
|
||||
{
|
||||
class PrepoMain : IService
|
||||
{
|
||||
public static void Main(ServiceTable serviceTable)
|
||||
{
|
||||
PrepoIpcServer ipcServer = new();
|
||||
|
||||
ipcServer.Initialize();
|
||||
|
||||
serviceTable.SignalServiceReady();
|
||||
|
||||
ipcServer.ServiceRequests();
|
||||
ipcServer.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
15
Ryujinx.Horizon/Prepo/PrepoResult.cs
Normal file
15
Ryujinx.Horizon/Prepo/PrepoResult.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Ryujinx.Horizon.Common;
|
||||
|
||||
namespace Ryujinx.Horizon.Prepo
|
||||
{
|
||||
static class PrepoResult
|
||||
{
|
||||
private const int ModuleId = 129;
|
||||
|
||||
public static Result InvalidArgument => new(ModuleId, 1);
|
||||
public static Result InvalidState => new(ModuleId, 5);
|
||||
public static Result InvalidBufferSize => new(ModuleId, 9);
|
||||
public static Result PermissionDenied => new(ModuleId, 90);
|
||||
public static Result InvalidPid => new(ModuleId, 101);
|
||||
}
|
||||
}
|
30
Ryujinx.Horizon/Prepo/PrepoServerManager.cs
Normal file
30
Ryujinx.Horizon/Prepo/PrepoServerManager.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Horizon.Prepo.Ipc;
|
||||
using Ryujinx.Horizon.Prepo.Types;
|
||||
using Ryujinx.Horizon.Sdk.Sf.Hipc;
|
||||
using Ryujinx.Horizon.Sdk.Sm;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Horizon.Prepo
|
||||
{
|
||||
class PrepoServerManager : ServerManager
|
||||
{
|
||||
public PrepoServerManager(HeapAllocator allocator, SmApi sm, int maxPorts, ManagerOptions options, int maxSessions) : base(allocator, sm, maxPorts, options, maxSessions)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Result OnNeedsToAccept(int portIndex, Server server)
|
||||
{
|
||||
return (PrepoPortIndex)portIndex switch
|
||||
{
|
||||
PrepoPortIndex.Admin => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Admin)),
|
||||
PrepoPortIndex.Admin2 => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Admin)),
|
||||
PrepoPortIndex.Manager => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Manager)),
|
||||
PrepoPortIndex.User => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.User)),
|
||||
PrepoPortIndex.System => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.System)),
|
||||
PrepoPortIndex.Debug => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Debug)),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(portIndex)),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
12
Ryujinx.Horizon/Prepo/Types/PrepoPortIndex.cs
Normal file
12
Ryujinx.Horizon/Prepo/Types/PrepoPortIndex.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Ryujinx.Horizon.Prepo.Types
|
||||
{
|
||||
enum PrepoPortIndex
|
||||
{
|
||||
Admin,
|
||||
Admin2,
|
||||
Manager,
|
||||
User,
|
||||
System,
|
||||
Debug
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Prepo
|
||||
namespace Ryujinx.Horizon.Prepo.Types
|
||||
{
|
||||
enum PrepoServicePermissionLevel
|
||||
{
|
||||
Admin = -1,
|
||||
User = 1,
|
||||
System = 2,
|
||||
Manager = 6
|
||||
Manager = 6,
|
||||
Debug = unchecked((int)0x80000006)
|
||||
}
|
||||
}
|
@ -11,4 +11,8 @@
|
||||
<ProjectReference Include="..\Ryujinx.Memory\Ryujinx.Memory.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LibHac" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
62
Ryujinx.Horizon/Sdk/Account/Uid.cs
Normal file
62
Ryujinx.Horizon/Sdk/Account/Uid.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Horizon.Sdk.Account
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct Uid
|
||||
{
|
||||
public readonly long High;
|
||||
public readonly long Low;
|
||||
|
||||
public bool IsNull => (Low | High) == 0;
|
||||
|
||||
public static Uid Null => new(0, 0);
|
||||
|
||||
public Uid(long low, long high)
|
||||
{
|
||||
Low = low;
|
||||
High = high;
|
||||
}
|
||||
|
||||
public Uid(byte[] bytes)
|
||||
{
|
||||
High = BitConverter.ToInt64(bytes, 0);
|
||||
Low = BitConverter.ToInt64(bytes, 8);
|
||||
}
|
||||
|
||||
public Uid(string hex)
|
||||
{
|
||||
if (hex == null || hex.Length != 32 || !hex.All("0123456789abcdefABCDEF".Contains))
|
||||
{
|
||||
throw new ArgumentException("Invalid Hex value!", nameof(hex));
|
||||
}
|
||||
|
||||
Low = Convert.ToInt64(hex[16..], 16);
|
||||
High = Convert.ToInt64(hex[..16], 16);
|
||||
}
|
||||
|
||||
public void Write(BinaryWriter binaryWriter)
|
||||
{
|
||||
binaryWriter.Write(High);
|
||||
binaryWriter.Write(Low);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return High.ToString("x16") + Low.ToString("x16");
|
||||
}
|
||||
|
||||
public LibHac.Account.Uid ToLibHacUid()
|
||||
{
|
||||
return new LibHac.Account.Uid((ulong)High, (ulong)Low);
|
||||
}
|
||||
|
||||
public UInt128 ToUInt128()
|
||||
{
|
||||
return new UInt128((ulong)High, (ulong)Low);
|
||||
}
|
||||
}
|
||||
}
|
12
Ryujinx.Horizon/Sdk/Lm/ILmLogger.cs
Normal file
12
Ryujinx.Horizon/Sdk/Lm/ILmLogger.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Horizon.Sdk.Sf;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Horizon.Sdk.Lm
|
||||
{
|
||||
interface ILmLogger : IServiceObject
|
||||
{
|
||||
Result Log(Span<byte> message);
|
||||
Result SetDestination(LogDestination destination);
|
||||
}
|
||||
}
|
11
Ryujinx.Horizon/Sdk/Lm/ILogService.cs
Normal file
11
Ryujinx.Horizon/Sdk/Lm/ILogService.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Horizon.LogManager.Ipc;
|
||||
using Ryujinx.Horizon.Sdk.Sf;
|
||||
|
||||
namespace Ryujinx.Horizon.Sdk.Lm
|
||||
{
|
||||
interface ILogService : IServiceObject
|
||||
{
|
||||
Result OpenLogger(out LmLogger logger, ulong pid);
|
||||
}
|
||||
}
|
20
Ryujinx.Horizon/Sdk/Prepo/IPrepoService.cs
Normal file
20
Ryujinx.Horizon/Sdk/Prepo/IPrepoService.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Horizon.Sdk.Account;
|
||||
using Ryujinx.Horizon.Sdk.Sf;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Horizon.Sdk.Prepo
|
||||
{
|
||||
interface IPrepoService : IServiceObject
|
||||
{
|
||||
Result SaveReport(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveReportWithUser(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result RequestImmediateTransmission();
|
||||
Result GetTransmissionStatus(out int status);
|
||||
Result GetSystemSessionId(out ulong systemSessionId);
|
||||
Result SaveSystemReport(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveSystemReportWithUser(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result IsUserAgreementCheckEnabled(out bool enabled);
|
||||
Result SetUserAgreementCheckEnabled(bool enabled);
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ namespace Ryujinx.Horizon.Sdk
|
||||
if (result.IsFailure)
|
||||
{
|
||||
response = default;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -22,14 +22,17 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
|
||||
totalSize += Unsafe.SizeOf<CmifInHeader>() + format.DataSize;
|
||||
totalSize = (totalSize + 1) & ~1;
|
||||
|
||||
int outPointerSizeTableOffset = totalSize;
|
||||
int outPointerSizeTableSize = format.OutAutoBuffersCount + format.OutPointersCount;
|
||||
|
||||
totalSize += sizeof(ushort) * outPointerSizeTableSize;
|
||||
|
||||
int rawDataSizeInWords = (totalSize + sizeof(uint) - 1) / sizeof(uint);
|
||||
|
||||
CmifRequest request = new CmifRequest();
|
||||
|
||||
request.Hipc = HipcMessage.WriteMessage(output, new HipcMetadata()
|
||||
CmifRequest request = new()
|
||||
{
|
||||
Hipc = HipcMessage.WriteMessage(output, new HipcMetadata()
|
||||
{
|
||||
Type = format.Context != 0 ? (int)CommandType.RequestWithContext : (int)CommandType.Request,
|
||||
SendStaticsCount = format.InAutoBuffersCount + format.InPointersCount,
|
||||
@ -41,7 +44,8 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
SendPid = format.SendPid,
|
||||
CopyHandlesCount = format.HandlesCount,
|
||||
MoveHandlesCount = 0
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
Span<uint> data = request.Hipc.DataWords;
|
||||
|
||||
@ -61,9 +65,9 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
Token = format.Context
|
||||
};
|
||||
|
||||
data = data.Slice(Unsafe.SizeOf<CmifDomainInHeader>() / sizeof(uint));
|
||||
data = data[(Unsafe.SizeOf<CmifDomainInHeader>() / sizeof(uint))..];
|
||||
|
||||
request.Objects = data.Slice((payloadSize + sizeof(uint) - 1) / sizeof(uint));
|
||||
request.Objects = data[((payloadSize + sizeof(uint) - 1) / sizeof(uint))..];
|
||||
}
|
||||
|
||||
ref CmifInHeader header = ref MemoryMarshal.Cast<uint, CmifInHeader>(data)[0];
|
||||
@ -76,11 +80,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
Token = format.ObjectId != 0 ? 0u : format.Context
|
||||
};
|
||||
|
||||
request.Data = MemoryMarshal.Cast<uint, byte>(data).Slice(Unsafe.SizeOf<CmifInHeader>());
|
||||
request.Data = MemoryMarshal.Cast<uint, byte>(data)[Unsafe.SizeOf<CmifInHeader>()..];
|
||||
|
||||
int paddingSizeBefore = (rawDataSizeInWords - request.Hipc.DataWords.Length) * sizeof(uint);
|
||||
|
||||
Span<byte> outPointerTable = MemoryMarshal.Cast<uint, byte>(request.Hipc.DataWords).Slice(outPointerSizeTableOffset - paddingSizeBefore);
|
||||
Span<byte> outPointerTable = MemoryMarshal.Cast<uint, byte>(request.Hipc.DataWords)[(outPointerSizeTableOffset - paddingSizeBefore)..];
|
||||
|
||||
request.OutPointerSizes = MemoryMarshal.Cast<byte, ushort>(outPointerTable);
|
||||
request.ServerPointerSize = format.ServerPointerSize;
|
||||
|
||||
@ -89,15 +94,15 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
|
||||
public static Result ParseResponse(out CmifResponse response, Span<byte> input, bool isDomain, int size)
|
||||
{
|
||||
HipcMessage responseMessage = new HipcMessage(input);
|
||||
HipcMessage responseMessage = new(input);
|
||||
|
||||
Span<byte> data = MemoryMarshal.Cast<uint, byte>(responseMessage.Data.DataWords);
|
||||
Span<uint> objects = Span<uint>.Empty;
|
||||
|
||||
if (isDomain)
|
||||
{
|
||||
data = data.Slice(Unsafe.SizeOf<CmifDomainOutHeader>());
|
||||
objects = MemoryMarshal.Cast<byte, uint>(data.Slice(Unsafe.SizeOf<CmifOutHeader>() + size));
|
||||
data = data[Unsafe.SizeOf<CmifDomainOutHeader>()..];
|
||||
objects = MemoryMarshal.Cast<byte, uint>(data[(Unsafe.SizeOf<CmifOutHeader>() + size)..]);
|
||||
}
|
||||
|
||||
CmifOutHeader header = MemoryMarshal.Cast<byte, CmifOutHeader>(data)[0];
|
||||
@ -105,18 +110,20 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
if (header.Magic != CmifOutHeaderMagic)
|
||||
{
|
||||
response = default;
|
||||
|
||||
return SfResult.InvalidOutHeader;
|
||||
}
|
||||
|
||||
if (header.Result.IsFailure)
|
||||
{
|
||||
response = default;
|
||||
|
||||
return header.Result;
|
||||
}
|
||||
|
||||
response = new CmifResponse()
|
||||
{
|
||||
Data = data.Slice(Unsafe.SizeOf<CmifOutHeader>()),
|
||||
Data = data[Unsafe.SizeOf<CmifOutHeader>()..],
|
||||
Objects = objects,
|
||||
CopyHandles = responseMessage.Data.CopyHandles,
|
||||
MoveHandles = responseMessage.Data.MoveHandles
|
||||
|
@ -21,7 +21,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
|
||||
var inHeader = MemoryMarshal.Cast<byte, CmifDomainInHeader>(inRawData)[0];
|
||||
|
||||
ReadOnlySpan<byte> inDomainRawData = inRawData.Slice(Unsafe.SizeOf<CmifDomainInHeader>());
|
||||
ReadOnlySpan<byte> inDomainRawData = inRawData[Unsafe.SizeOf<CmifDomainInHeader>()..];
|
||||
|
||||
int targetObjectId = inHeader.ObjectId;
|
||||
|
||||
@ -39,7 +39,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
return SfResult.InvalidHeaderSize;
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> inMessageRawData = inDomainRawData.Slice(0, inHeader.DataSize);
|
||||
ReadOnlySpan<byte> inMessageRawData = inDomainRawData[..inHeader.DataSize];
|
||||
|
||||
if (inHeader.ObjectsCount > DomainServiceObjectProcessor.MaximumObjects)
|
||||
{
|
||||
|
@ -63,7 +63,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
return SfResult.InvalidInObjectsCount;
|
||||
}
|
||||
|
||||
Result result = _domain.ReserveIds(new Span<int>(_reservedObjectIds).Slice(0, OutObjectsCount));
|
||||
Result result = _domain.ReserveIds(new Span<int>(_reservedObjectIds)[..OutObjectsCount]);
|
||||
|
||||
if (result.IsFailure)
|
||||
{
|
||||
@ -92,7 +92,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
|
||||
DebugUtil.Assert(outHeaderSize + implOutDataTotalSize + OutObjectsCount * sizeof(int) <= outRawData.Length);
|
||||
|
||||
outRawData = outRawData.Slice(outHeaderSize);
|
||||
outRawData = outRawData[outHeaderSize..];
|
||||
_outObjectIdsOffset = (response.DataWords.Length * sizeof(uint) - outRawData.Length) + implOutDataTotalSize;
|
||||
|
||||
return response;
|
||||
@ -107,9 +107,9 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
|
||||
DebugUtil.Assert(outHeaderSize + implOutDataTotalSize <= outRawData.Length);
|
||||
|
||||
outRawData = outRawData.Slice(outHeaderSize);
|
||||
outRawData = outRawData[outHeaderSize..];
|
||||
|
||||
_domain.UnreserveIds(new Span<int>(_reservedObjectIds).Slice(0, OutObjectsCount));
|
||||
_domain.UnreserveIds(new Span<int>(_reservedObjectIds)[..OutObjectsCount]);
|
||||
}
|
||||
|
||||
public override void SetOutObjects(scoped ref ServiceDispatchContext context, HipcMessageData response, Span<ServiceObjectHolder> outObjects)
|
||||
@ -129,7 +129,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
_domain.RegisterObject(objectIds[i], outObjects[i]);
|
||||
}
|
||||
|
||||
Span<int> outObjectIds = MemoryMarshal.Cast<byte, int>(MemoryMarshal.Cast<uint, byte>(response.DataWords).Slice(_outObjectIdsOffset));
|
||||
Span<int> outObjectIds = MemoryMarshal.Cast<byte, int>(MemoryMarshal.Cast<uint, byte>(response.DataWords)[_outObjectIdsOffset..]);
|
||||
|
||||
for (int i = 0; i < outObjectsCount; i++)
|
||||
{
|
||||
|
@ -2,7 +2,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
{
|
||||
struct PointerAndSize
|
||||
{
|
||||
public static PointerAndSize Empty => new PointerAndSize(0UL, 0UL);
|
||||
public static PointerAndSize Empty => new(0UL, 0UL);
|
||||
|
||||
public ulong Address { get; }
|
||||
public ulong Size { get; }
|
||||
|
@ -8,6 +8,7 @@
|
||||
public byte OutHeadersSize { get; }
|
||||
public byte InObjectsCount { get; }
|
||||
public byte OutObjectsCount { get; }
|
||||
|
||||
public int UnfixedOutPointerSizeOffset => InDataSize + InHeadersSize + 0x10;
|
||||
|
||||
public ServerMessageRuntimeMetadata(
|
||||
|
@ -39,17 +39,21 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
|
||||
if (!entries.TryGetValue((int)commandId, out var commandHandler))
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.KernelIpc, $"{objectName} command ID 0x{commandId:X} is not implemented");
|
||||
|
||||
if (HorizonStatic.Options.IgnoreMissingServices)
|
||||
{
|
||||
// If ignore missing services is enabled, just pretend that everything is fine.
|
||||
var response = PrepareForStubReply(ref context, out Span<byte> outRawData);
|
||||
PrepareForStubReply(ref context, out Span<byte> outRawData);
|
||||
CommandHandler.GetCmifOutHeaderPointer(ref outHeader, ref outRawData);
|
||||
outHeader[0] = new CmifOutHeader() { Magic = CmifMessage.CmifOutHeaderMagic, Result = Result.Success };
|
||||
|
||||
Logger.Warning?.Print(LogClass.Service, $"Missing service {objectName} (command ID: {commandId}) ignored");
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
else if (HorizonStatic.Options.ThrowOnInvalidCommandIds)
|
||||
{
|
||||
throw new NotImplementedException($"{objectName} command ID: {commandId} is not implemented");
|
||||
}
|
||||
|
||||
return SfResult.UnknownCommandId;
|
||||
}
|
||||
@ -72,6 +76,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
if (outHeader.IsEmpty)
|
||||
{
|
||||
commandResult.AbortOnSuccess();
|
||||
|
||||
return commandResult;
|
||||
}
|
||||
|
||||
@ -80,11 +85,10 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static HipcMessageData PrepareForStubReply(scoped ref ServiceDispatchContext context, out Span<byte> outRawData)
|
||||
private static void PrepareForStubReply(scoped ref ServiceDispatchContext context, out Span<byte> outRawData)
|
||||
{
|
||||
var response = HipcMessage.WriteResponse(context.OutMessageBuffer, 0, 0x20 / sizeof(uint), 0, 0);
|
||||
outRawData = MemoryMarshal.Cast<uint, byte>(response.DataWords);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
@ -50,8 +50,8 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
|
||||
public static void GetCmifOutHeaderPointer(ref Span<CmifOutHeader> outHeader, ref Span<byte> outRawData)
|
||||
{
|
||||
outHeader = MemoryMarshal.Cast<byte, CmifOutHeader>(outRawData).Slice(0, 1);
|
||||
outRawData = outRawData.Slice(Unsafe.SizeOf<CmifOutHeader>());
|
||||
outHeader = MemoryMarshal.Cast<byte, CmifOutHeader>(outRawData)[..1];
|
||||
outRawData = outRawData[Unsafe.SizeOf<CmifOutHeader>()..];
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
public static ref T GetRef<T>(PointerAndSize bufferRange) where T : unmanaged
|
||||
{
|
||||
var writableRegion = GetWritableRegion(bufferRange);
|
||||
|
||||
return ref MemoryMarshal.Cast<byte, T>(writableRegion.Memory.Span)[0];
|
||||
}
|
||||
|
||||
|
@ -51,22 +51,18 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
Result result = ReplyImpl(sessionHandle, messageBuffer);
|
||||
|
||||
result.AbortOnFailureUnless(KernelResult.TimedOut, KernelResult.PortRemoteClosed);
|
||||
result.AbortUnless(KernelResult.TimedOut, KernelResult.PortRemoteClosed);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private static Result ReplyImpl(int sessionHandle, ReadOnlySpan<byte> messageBuffer)
|
||||
{
|
||||
Span<int> handles = stackalloc int[1];
|
||||
|
||||
handles[0] = sessionHandle;
|
||||
|
||||
var tlsSpan = HorizonStatic.AddressSpace.GetSpan(HorizonStatic.ThreadContext.TlsAddress, TlsMessageBufferSize);
|
||||
|
||||
if (messageBuffer == tlsSpan)
|
||||
{
|
||||
return HorizonStatic.Syscall.ReplyAndReceive(out _, handles, sessionHandle, 0);
|
||||
return HorizonStatic.Syscall.ReplyAndReceive(out _, ReadOnlySpan<int>.Empty, sessionHandle, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -61,7 +61,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
clientHandle = 0;
|
||||
|
||||
if (!(_session.ServiceObjectHolder.ServiceObject is DomainServiceObject domain))
|
||||
if (_session.ServiceObjectHolder.ServiceObject is not DomainServiceObject domain)
|
||||
{
|
||||
return HipcResult.TargetNotDomain;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
|
||||
Header header = MemoryMarshal.Cast<byte, Header>(data)[0];
|
||||
|
||||
data = data.Slice(Unsafe.SizeOf<Header>());
|
||||
data = data[Unsafe.SizeOf<Header>()..];
|
||||
|
||||
int receiveStaticsCount = 0;
|
||||
ulong pid = 0;
|
||||
@ -42,14 +42,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
if (header.HasSpecialHeader)
|
||||
{
|
||||
specialHeader = MemoryMarshal.Cast<byte, SpecialHeader>(data)[0];
|
||||
|
||||
data = data.Slice(Unsafe.SizeOf<SpecialHeader>());
|
||||
data = data[Unsafe.SizeOf<SpecialHeader>()..];
|
||||
|
||||
if (specialHeader.SendPid)
|
||||
{
|
||||
pid = MemoryMarshal.Cast<byte, ulong>(data)[0];
|
||||
|
||||
data = data.Slice(sizeof(ulong));
|
||||
data = data[sizeof(ulong)..];
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,7 +88,6 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
public static HipcMessageData WriteMessage(Span<byte> destination, HipcMetadata meta)
|
||||
{
|
||||
int initialLength = destination.Length;
|
||||
|
||||
bool hasSpecialHeader = meta.SendPid || meta.CopyHandlesCount != 0 || meta.MoveHandlesCount != 0;
|
||||
|
||||
MemoryMarshal.Cast<byte, Header>(destination)[0] = new Header()
|
||||
@ -105,7 +102,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
HasSpecialHeader = hasSpecialHeader
|
||||
};
|
||||
|
||||
destination = destination.Slice(Unsafe.SizeOf<Header>());
|
||||
destination = destination[Unsafe.SizeOf<Header>()..];
|
||||
|
||||
if (hasSpecialHeader)
|
||||
{
|
||||
@ -116,11 +113,11 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
MoveHandlesCount = meta.MoveHandlesCount
|
||||
};
|
||||
|
||||
destination = destination.Slice(Unsafe.SizeOf<SpecialHeader>());
|
||||
destination = destination[Unsafe.SizeOf<SpecialHeader>()..];
|
||||
|
||||
if (meta.SendPid)
|
||||
{
|
||||
destination = destination.Slice(sizeof(ulong));
|
||||
destination = destination[sizeof(ulong)..];
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,54 +130,54 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
|
||||
if (meta.CopyHandlesCount != 0)
|
||||
{
|
||||
copyHandles = MemoryMarshal.Cast<byte, int>(data).Slice(0, meta.CopyHandlesCount);
|
||||
copyHandles = MemoryMarshal.Cast<byte, int>(data)[..meta.CopyHandlesCount];
|
||||
|
||||
data = data.Slice(meta.CopyHandlesCount * sizeof(int));
|
||||
data = data[(meta.CopyHandlesCount * sizeof(int))..];
|
||||
}
|
||||
|
||||
Span<int> moveHandles = Span<int>.Empty;
|
||||
|
||||
if (meta.MoveHandlesCount != 0)
|
||||
{
|
||||
moveHandles = MemoryMarshal.Cast<byte, int>(data).Slice(0, meta.MoveHandlesCount);
|
||||
moveHandles = MemoryMarshal.Cast<byte, int>(data)[..meta.MoveHandlesCount];
|
||||
|
||||
data = data.Slice(meta.MoveHandlesCount * sizeof(int));
|
||||
data = data[(meta.MoveHandlesCount * sizeof(int))..];
|
||||
}
|
||||
|
||||
Span<HipcStaticDescriptor> sendStatics = Span<HipcStaticDescriptor>.Empty;
|
||||
|
||||
if (meta.SendStaticsCount != 0)
|
||||
{
|
||||
sendStatics = MemoryMarshal.Cast<byte, HipcStaticDescriptor>(data).Slice(0, meta.SendStaticsCount);
|
||||
sendStatics = MemoryMarshal.Cast<byte, HipcStaticDescriptor>(data)[..meta.SendStaticsCount];
|
||||
|
||||
data = data.Slice(meta.SendStaticsCount * Unsafe.SizeOf<HipcStaticDescriptor>());
|
||||
data = data[(meta.SendStaticsCount * Unsafe.SizeOf<HipcStaticDescriptor>())..];
|
||||
}
|
||||
|
||||
Span<HipcBufferDescriptor> sendBuffers = Span<HipcBufferDescriptor>.Empty;
|
||||
|
||||
if (meta.SendBuffersCount != 0)
|
||||
{
|
||||
sendBuffers = MemoryMarshal.Cast<byte, HipcBufferDescriptor>(data).Slice(0, meta.SendBuffersCount);
|
||||
sendBuffers = MemoryMarshal.Cast<byte, HipcBufferDescriptor>(data)[..meta.SendBuffersCount];
|
||||
|
||||
data = data.Slice(meta.SendBuffersCount * Unsafe.SizeOf<HipcBufferDescriptor>());
|
||||
data = data[(meta.SendBuffersCount * Unsafe.SizeOf<HipcBufferDescriptor>())..];
|
||||
}
|
||||
|
||||
Span<HipcBufferDescriptor> receiveBuffers = Span<HipcBufferDescriptor>.Empty;
|
||||
|
||||
if (meta.ReceiveBuffersCount != 0)
|
||||
{
|
||||
receiveBuffers = MemoryMarshal.Cast<byte, HipcBufferDescriptor>(data).Slice(0, meta.ReceiveBuffersCount);
|
||||
receiveBuffers = MemoryMarshal.Cast<byte, HipcBufferDescriptor>(data)[..meta.ReceiveBuffersCount];
|
||||
|
||||
data = data.Slice(meta.ReceiveBuffersCount * Unsafe.SizeOf<HipcBufferDescriptor>());
|
||||
data = data[(meta.ReceiveBuffersCount * Unsafe.SizeOf<HipcBufferDescriptor>())..];
|
||||
}
|
||||
|
||||
Span<HipcBufferDescriptor> exchangeBuffers = Span<HipcBufferDescriptor>.Empty;
|
||||
|
||||
if (meta.ExchangeBuffersCount != 0)
|
||||
{
|
||||
exchangeBuffers = MemoryMarshal.Cast<byte, HipcBufferDescriptor>(data).Slice(0, meta.ExchangeBuffersCount);
|
||||
exchangeBuffers = MemoryMarshal.Cast<byte, HipcBufferDescriptor>(data)[..meta.ExchangeBuffersCount];
|
||||
|
||||
data = data.Slice(meta.ExchangeBuffersCount * Unsafe.SizeOf<HipcBufferDescriptor>());
|
||||
data = data[(meta.ExchangeBuffersCount * Unsafe.SizeOf<HipcBufferDescriptor>())..];
|
||||
}
|
||||
|
||||
Span<uint> dataWords = Span<uint>.Empty;
|
||||
@ -189,12 +186,11 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
int dataOffset = initialLength - data.Length;
|
||||
int dataOffsetAligned = BitUtils.AlignUp(dataOffset, 0x10);
|
||||
|
||||
int padding = (dataOffsetAligned - dataOffset) / sizeof(uint);
|
||||
|
||||
dataWords = MemoryMarshal.Cast<byte, uint>(data).Slice(padding, meta.DataWordsCount - padding);
|
||||
dataWords = MemoryMarshal.Cast<byte, uint>(data)[padding..meta.DataWordsCount];
|
||||
|
||||
data = data.Slice(meta.DataWordsCount * sizeof(uint));
|
||||
data = data[(meta.DataWordsCount * sizeof(uint))..];
|
||||
}
|
||||
|
||||
Span<HipcReceiveListEntry> receiveList = Span<HipcReceiveListEntry>.Empty;
|
||||
@ -203,7 +199,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
int receiveListSize = meta.ReceiveStaticsCount == AutoReceiveStatic ? 1 : meta.ReceiveStaticsCount;
|
||||
|
||||
receiveList = MemoryMarshal.Cast<byte, HipcReceiveListEntry>(data).Slice(0, receiveListSize);
|
||||
receiveList = MemoryMarshal.Cast<byte, HipcReceiveListEntry>(data)[..receiveListSize];
|
||||
}
|
||||
|
||||
return new HipcMessageData()
|
||||
|
@ -6,17 +6,14 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
public const int ModuleId = 11;
|
||||
|
||||
public static Result OutOfSessionMemory => new Result(ModuleId, 102);
|
||||
public static Result OutOfSessions => new Result(ModuleId, 131);
|
||||
public static Result PointerBufferTooSmall => new Result(ModuleId, 141);
|
||||
public static Result OutOfDomains => new Result(ModuleId, 200);
|
||||
|
||||
public static Result InvalidRequestSize => new Result(ModuleId, 402);
|
||||
public static Result UnknownCommandType => new Result(ModuleId, 403);
|
||||
|
||||
public static Result InvalidCmifRequest => new Result(ModuleId, 420);
|
||||
|
||||
public static Result TargetNotDomain => new Result(ModuleId, 491);
|
||||
public static Result DomainObjectNotFound => new Result(ModuleId, 492);
|
||||
public static Result OutOfSessionMemory => new(ModuleId, 102);
|
||||
public static Result OutOfSessions => new(ModuleId, 131);
|
||||
public static Result PointerBufferTooSmall => new(ModuleId, 141);
|
||||
public static Result OutOfDomains => new(ModuleId, 200);
|
||||
public static Result InvalidRequestSize => new(ModuleId, 402);
|
||||
public static Result UnknownCommandType => new(ModuleId, 403);
|
||||
public static Result InvalidCmifRequest => new(ModuleId, 420);
|
||||
public static Result TargetNotDomain => new(ModuleId, 491);
|
||||
public static Result DomainObjectNotFound => new(ModuleId, 492);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
{
|
||||
struct ManagerOptions
|
||||
{
|
||||
public static ManagerOptions Default => new ManagerOptions(0, 0, 0, false);
|
||||
public static ManagerOptions Default => new(0, 0, 0, false);
|
||||
|
||||
public int PointerBufferSize { get; }
|
||||
public int MaxDomains { get; }
|
||||
|
@ -15,7 +15,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
|
||||
protected override Result DispatchManagerRequest(ServerSession session, Span<byte> inMessage, Span<byte> outMessage)
|
||||
{
|
||||
HipcManager hipcManager = new HipcManager(this, session);
|
||||
HipcManager hipcManager = new(this, session);
|
||||
|
||||
return DispatchRequest(new ServiceObjectHolder(hipcManager), session, inMessage, outMessage);
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
return null;
|
||||
}
|
||||
|
||||
ServerSession session = new ServerSession(sessionIndex, sessionHandle, obj);
|
||||
ServerSession session = new(sessionIndex, sessionHandle, obj);
|
||||
|
||||
_sessions.Add(session);
|
||||
|
||||
@ -111,7 +111,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
lock (_resourceLock)
|
||||
{
|
||||
Server server = new Server(portIndex, portHandle, name, managed, staticHoder);
|
||||
Server server = new(portIndex, portHandle, name, managed, staticHoder);
|
||||
|
||||
_servers.Add(server);
|
||||
|
||||
|
@ -46,6 +46,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
|
||||
_requestStopEventHolder = new MultiWaitHolderOfEvent(_requestStopEvent);
|
||||
_multiWait.LinkMultiWaitHolder(_requestStopEventHolder);
|
||||
|
||||
_notifyEventHolder = new MultiWaitHolderOfEvent(_notifyEvent);
|
||||
_multiWait.LinkMultiWaitHolder(_notifyEventHolder);
|
||||
}
|
||||
@ -73,6 +74,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
private void RegisterServerImpl(int portIndex, ServiceObjectHolder staticHolder, int portHandle)
|
||||
{
|
||||
Server server = AllocateServer(portIndex, portHandle, ServiceName.Invalid, managed: false, staticHolder);
|
||||
|
||||
RegisterServerImpl(server);
|
||||
}
|
||||
|
||||
@ -86,6 +88,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
}
|
||||
|
||||
Server server = AllocateServer(portIndex, portHandle, name, managed: true, staticHolder);
|
||||
|
||||
RegisterServerImpl(server);
|
||||
|
||||
return Result.Success;
|
||||
@ -103,6 +106,11 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
protected Result AcceptImpl(Server server, IServiceObject obj)
|
||||
{
|
||||
return AcceptSession(server.PortHandle, new ServiceObjectHolder(obj));
|
||||
}
|
||||
|
||||
public void ServiceRequests()
|
||||
{
|
||||
while (WaitAndProcessRequestsImpl());
|
||||
@ -176,6 +184,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
session.HasReceived = false;
|
||||
session.UserData = UserDataTag.Session;
|
||||
|
||||
RegisterToWaitList(session);
|
||||
}
|
||||
|
||||
@ -198,15 +207,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
|
||||
private Result Process(MultiWaitHolder holder)
|
||||
{
|
||||
switch ((UserDataTag)holder.UserData)
|
||||
return (UserDataTag)holder.UserData switch
|
||||
{
|
||||
case UserDataTag.Server:
|
||||
return ProcessForServer(holder);
|
||||
case UserDataTag.Session:
|
||||
return ProcessForSession(holder);
|
||||
default:
|
||||
throw new NotImplementedException(((UserDataTag)holder.UserData).ToString());
|
||||
}
|
||||
UserDataTag.Server => ProcessForServer(holder),
|
||||
UserDataTag.Session => ProcessForSession(holder),
|
||||
_ => throw new NotImplementedException(((UserDataTag)holder.UserData).ToString())
|
||||
};
|
||||
}
|
||||
|
||||
private Result ProcessForServer(MultiWaitHolder holder)
|
||||
@ -259,6 +265,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
}
|
||||
|
||||
session.HasReceived = true;
|
||||
|
||||
tlsMessage.Memory.Span.CopyTo(savedMessage.Memory.Span);
|
||||
}
|
||||
else
|
||||
|
@ -78,6 +78,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
session.SavedMessage = GetSessionSavedMessageBuffer(session);
|
||||
|
||||
RegisterSessionToWaitList(session);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
@ -141,6 +142,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
protected void CloseSessionImpl(ServerSession session)
|
||||
{
|
||||
int sessionHandle = session.Handle;
|
||||
|
||||
Os.FinalizeMultiWaitHolder(session);
|
||||
DestroySession(session);
|
||||
HorizonStatic.Syscall.CloseHandle(sessionHandle).AbortOnFailure();
|
||||
@ -156,6 +158,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
if (session.IsClosed || GetCmifCommandType(message) == CommandType.Close)
|
||||
{
|
||||
CloseSessionImpl(session);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
else
|
||||
@ -165,6 +168,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
RegisterSessionToWaitList(session);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
else if (SfResult.RequestContextChanged(result))
|
||||
@ -176,6 +180,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
Logger.Warning?.Print(LogClass.KernelIpc, $"Request processing returned error {result}");
|
||||
|
||||
CloseSessionImpl(session);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
@ -190,6 +190,7 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
var descriptor = context.Request.Data.SendStatics[sendPointerIndex++];
|
||||
ulong address = descriptor.Address;
|
||||
ulong size = descriptor.Size;
|
||||
|
||||
_bufferRanges[i] = new PointerAndSize(address, size);
|
||||
|
||||
if (size != 0)
|
||||
@ -208,7 +209,8 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
else
|
||||
{
|
||||
var data = MemoryMarshal.Cast<uint, byte>(context.Request.Data.DataWords);
|
||||
var recvPointerSizes = MemoryMarshal.Cast<byte, ushort>(data.Slice(runtimeMetadata.UnfixedOutPointerSizeOffset));
|
||||
var recvPointerSizes = MemoryMarshal.Cast<byte, ushort>(data[runtimeMetadata.UnfixedOutPointerSizeOffset..]);
|
||||
|
||||
size = recvPointerSizes[unfixedRecvPointerIndex++];
|
||||
}
|
||||
|
||||
@ -314,6 +316,7 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
|
||||
int rawSizeInBytes = meta.DataWordsCount * sizeof(uint);
|
||||
int commandRawSize = BitUtils.AlignUp(runtimeMetadata.UnfixedOutPointerSizeOffset + (OutUnfixedSizePointerBuffersCount * sizeof(ushort)), sizeof(uint));
|
||||
|
||||
requestValid &= rawSizeInBytes >= commandRawSize;
|
||||
|
||||
return requestValid ? Result.Success : HipcResult.InvalidCmifRequest;
|
||||
@ -365,6 +368,7 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
_outCopyHandlesCount,
|
||||
_outMoveHandlesCount + runtimeMetadata.OutObjectsCount);
|
||||
outRawData = MemoryMarshal.Cast<uint, byte>(response.DataWords);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -377,6 +381,7 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
(BitUtils.AlignUp(rawDataSize, 4) + 0x10) / sizeof(uint),
|
||||
0,
|
||||
0);
|
||||
|
||||
outRawData = MemoryMarshal.Cast<uint, byte>(response.DataWords);
|
||||
}
|
||||
|
||||
@ -410,6 +415,7 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
if (obj == null)
|
||||
{
|
||||
response.MoveHandles[index] = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -27,9 +27,7 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
{
|
||||
for (int j = i; j > 0 && aligns[map[j - 1]] > aligns[map[j]]; j--)
|
||||
{
|
||||
var temp = map[j - 1];
|
||||
map[j - 1] = map[j];
|
||||
map[j] = temp;
|
||||
(map[j], map[j - 1]) = (map[j - 1], map[j]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,26 +6,22 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
{
|
||||
public const int ModuleId = 10;
|
||||
|
||||
public static Result NotSupported => new Result(ModuleId, 1);
|
||||
public static Result InvalidHeaderSize => new Result(ModuleId, 202);
|
||||
public static Result InvalidInHeader => new Result(ModuleId, 211);
|
||||
public static Result InvalidOutHeader => new Result(ModuleId, 212);
|
||||
public static Result UnknownCommandId => new Result(ModuleId, 221);
|
||||
public static Result InvalidOutRawSize => new Result(ModuleId, 232);
|
||||
public static Result InvalidInObjectsCount => new Result(ModuleId, 235);
|
||||
public static Result InvalidOutObjectsCount => new Result(ModuleId, 236);
|
||||
public static Result InvalidInObject => new Result(ModuleId, 239);
|
||||
|
||||
public static Result TargetNotFound => new Result(ModuleId, 261);
|
||||
|
||||
public static Result OutOfDomainEntries => new Result(ModuleId, 301);
|
||||
|
||||
public static Result InvalidatedByUser => new Result(ModuleId, 802);
|
||||
public static Result RequestDeferredByUser => new Result(ModuleId, 812);
|
||||
public static Result NotSupported => new(ModuleId, 1);
|
||||
public static Result InvalidHeaderSize => new(ModuleId, 202);
|
||||
public static Result InvalidInHeader => new(ModuleId, 211);
|
||||
public static Result InvalidOutHeader => new(ModuleId, 212);
|
||||
public static Result UnknownCommandId => new(ModuleId, 221);
|
||||
public static Result InvalidOutRawSize => new(ModuleId, 232);
|
||||
public static Result InvalidInObjectsCount => new(ModuleId, 235);
|
||||
public static Result InvalidOutObjectsCount => new(ModuleId, 236);
|
||||
public static Result InvalidInObject => new(ModuleId, 239);
|
||||
public static Result TargetNotFound => new(ModuleId, 261);
|
||||
public static Result OutOfDomainEntries => new(ModuleId, 301);
|
||||
public static Result InvalidatedByUser => new(ModuleId, 802);
|
||||
public static Result RequestDeferredByUser => new(ModuleId, 812);
|
||||
|
||||
public static bool RequestContextChanged(Result result) => result.InRange(800, 899);
|
||||
public static bool Invalidated(Result result) => result.InRange(801, 809);
|
||||
|
||||
public static bool RequestDeferred(Result result) => result.InRange(811, 819);
|
||||
}
|
||||
}
|
8
Ryujinx.Horizon/Sdk/Sm/IManagerService.cs
Normal file
8
Ryujinx.Horizon/Sdk/Sm/IManagerService.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Ryujinx.Horizon.Sdk.Sf;
|
||||
|
||||
namespace Ryujinx.Horizon.Sdk.Sm
|
||||
{
|
||||
interface IManagerService : IServiceObject
|
||||
{
|
||||
}
|
||||
}
|
13
Ryujinx.Horizon/Sdk/Sm/IUserService.cs
Normal file
13
Ryujinx.Horizon/Sdk/Sm/IUserService.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Horizon.Sdk.Sf;
|
||||
|
||||
namespace Ryujinx.Horizon.Sdk.Sm
|
||||
{
|
||||
interface IUserService : IServiceObject
|
||||
{
|
||||
Result Initialize(ulong clientProcessId);
|
||||
Result GetService(out int handle, ServiceName name);
|
||||
Result RegisterService(out int handle, ServiceName name, int maxSessions, bool isLight);
|
||||
Result UnregisterService(ServiceName name);
|
||||
}
|
||||
}
|
@ -7,16 +7,18 @@ namespace Ryujinx.Horizon.Sdk.Sm
|
||||
{
|
||||
class SmApi
|
||||
{
|
||||
private const string SmName = "sm:";
|
||||
|
||||
private int _portHandle;
|
||||
|
||||
public Result Initialize()
|
||||
{
|
||||
Result result = HorizonStatic.Syscall.ConnectToNamedPort(out int portHandle, "sm:");
|
||||
Result result = HorizonStatic.Syscall.ConnectToNamedPort(out int portHandle, SmName);
|
||||
|
||||
while (result == KernelResult.NotFound)
|
||||
{
|
||||
HorizonStatic.Syscall.SleepThread(50000000L);
|
||||
result = HorizonStatic.Syscall.ConnectToNamedPort(out portHandle, "sm:");
|
||||
result = HorizonStatic.Syscall.ConnectToNamedPort(out portHandle, SmName);
|
||||
}
|
||||
|
||||
if (result.IsFailure)
|
||||
@ -33,7 +35,7 @@ namespace Ryujinx.Horizon.Sdk.Sm
|
||||
{
|
||||
Span<byte> data = stackalloc byte[8];
|
||||
|
||||
SpanWriter writer = new SpanWriter(data);
|
||||
SpanWriter writer = new(data);
|
||||
|
||||
writer.Write(0UL);
|
||||
|
||||
@ -44,7 +46,7 @@ namespace Ryujinx.Horizon.Sdk.Sm
|
||||
{
|
||||
Span<byte> data = stackalloc byte[8];
|
||||
|
||||
SpanWriter writer = new SpanWriter(data);
|
||||
SpanWriter writer = new(data);
|
||||
|
||||
writer.Write(name);
|
||||
|
||||
@ -53,10 +55,12 @@ namespace Ryujinx.Horizon.Sdk.Sm
|
||||
if (result.IsFailure)
|
||||
{
|
||||
handle = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
handle = response.MoveHandles[0];
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
@ -64,7 +68,7 @@ namespace Ryujinx.Horizon.Sdk.Sm
|
||||
{
|
||||
Span<byte> data = stackalloc byte[16];
|
||||
|
||||
SpanWriter writer = new SpanWriter(data);
|
||||
SpanWriter writer = new(data);
|
||||
|
||||
writer.Write(name);
|
||||
writer.Write(isLight ? 1 : 0);
|
||||
@ -75,10 +79,12 @@ namespace Ryujinx.Horizon.Sdk.Sm
|
||||
if (result.IsFailure)
|
||||
{
|
||||
handle = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
handle = response.MoveHandles[0];
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
@ -86,7 +92,7 @@ namespace Ryujinx.Horizon.Sdk.Sm
|
||||
{
|
||||
Span<byte> data = stackalloc byte[8];
|
||||
|
||||
SpanWriter writer = new SpanWriter(data);
|
||||
SpanWriter writer = new(data);
|
||||
|
||||
writer.Write(name);
|
||||
|
||||
@ -97,7 +103,7 @@ namespace Ryujinx.Horizon.Sdk.Sm
|
||||
{
|
||||
Span<byte> data = stackalloc byte[8];
|
||||
|
||||
SpanWriter writer = new SpanWriter(data);
|
||||
SpanWriter writer = new(data);
|
||||
|
||||
writer.Write(0UL);
|
||||
|
||||
|
@ -6,12 +6,14 @@ namespace Ryujinx.Horizon
|
||||
{
|
||||
public struct ServiceEntry
|
||||
{
|
||||
private readonly Action _entrypoint;
|
||||
private readonly Action<ServiceTable> _entrypoint;
|
||||
private readonly ServiceTable _serviceTable;
|
||||
private readonly HorizonOptions _options;
|
||||
|
||||
internal ServiceEntry(Action entrypoint, HorizonOptions options)
|
||||
internal ServiceEntry(Action<ServiceTable> entrypoint, ServiceTable serviceTable, HorizonOptions options)
|
||||
{
|
||||
_entrypoint = entrypoint;
|
||||
_serviceTable = serviceTable;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
@ -19,7 +21,7 @@ namespace Ryujinx.Horizon
|
||||
{
|
||||
HorizonStatic.Register(_options, syscallApi, addressSpace, threadContext, (int)threadContext.GetX(1));
|
||||
|
||||
_entrypoint();
|
||||
_entrypoint(_serviceTable);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,58 @@
|
||||
using Ryujinx.Horizon.LogManager;
|
||||
using Ryujinx.Horizon.Prepo;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Ryujinx.Horizon
|
||||
{
|
||||
public static class ServiceTable
|
||||
public class ServiceTable
|
||||
{
|
||||
public static IEnumerable<ServiceEntry> GetServices(HorizonOptions options)
|
||||
private int _readyServices;
|
||||
private int _totalServices;
|
||||
|
||||
private readonly ManualResetEvent _servicesReadyEvent = new(false);
|
||||
|
||||
public IEnumerable<ServiceEntry> GetServices(HorizonOptions options)
|
||||
{
|
||||
List<ServiceEntry> entries = new List<ServiceEntry>();
|
||||
List<ServiceEntry> entries = new();
|
||||
|
||||
void RegisterService<T>() where T : IService
|
||||
{
|
||||
entries.Add(new ServiceEntry(T.Main, options));
|
||||
entries.Add(new ServiceEntry(T.Main, this, options));
|
||||
}
|
||||
|
||||
RegisterService<LmMain>();
|
||||
RegisterService<PrepoMain>();
|
||||
|
||||
_totalServices = entries.Count;
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
internal void SignalServiceReady()
|
||||
{
|
||||
if (Interlocked.Increment(ref _readyServices) == _totalServices)
|
||||
{
|
||||
_servicesReadyEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void WaitServicesReady()
|
||||
{
|
||||
_servicesReadyEvent.WaitOne();
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_servicesReadyEvent.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
}
|
||||
}
|
@ -126,20 +126,19 @@ namespace Ryujinx.Horizon.Sm.Impl
|
||||
// TODO: Validation with GetProcessInfo etc.
|
||||
|
||||
int serviceIndex = GetServiceInfo(name);
|
||||
|
||||
if (serviceIndex < 0)
|
||||
{
|
||||
return SmResult.NotRegistered;
|
||||
}
|
||||
|
||||
ref var serviceInfo = ref _services[serviceIndex];
|
||||
|
||||
if (serviceInfo.OwnerProcessId != processId)
|
||||
{
|
||||
return SmResult.NotAllowed;
|
||||
}
|
||||
|
||||
serviceInfo.Free();
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
|
8
Ryujinx.Horizon/Sm/Ipc/ManagerService.cs
Normal file
8
Ryujinx.Horizon/Sm/Ipc/ManagerService.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Ryujinx.Horizon.Sdk.Sm;
|
||||
|
||||
namespace Ryujinx.Horizon.Sm.Ipc
|
||||
{
|
||||
partial class ManagerService : IManagerService
|
||||
{
|
||||
}
|
||||
}
|
@ -3,9 +3,9 @@ using Ryujinx.Horizon.Sdk.Sf;
|
||||
using Ryujinx.Horizon.Sdk.Sm;
|
||||
using Ryujinx.Horizon.Sm.Impl;
|
||||
|
||||
namespace Ryujinx.Horizon.Sm
|
||||
namespace Ryujinx.Horizon.Sm.Ipc
|
||||
{
|
||||
partial class UserService : IServiceObject
|
||||
partial class UserService : IUserService
|
||||
{
|
||||
private readonly ServiceManager _serviceManager;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user