Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
550747eac6 | ||
|
3ffceab1fb | ||
|
b9f2a96595 | ||
|
cbaa845f5d | ||
|
9e2681f2d7 | ||
|
81fae0d1a6 | ||
|
38519f3b9a |
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
|
||||
|
||||
|
@@ -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);
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -711,4 +711,4 @@ namespace Ryujinx.Modules
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,8 +13,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 +34,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))
|
||||
{
|
||||
@@ -93,6 +137,12 @@ namespace Ryujinx.Ava
|
||||
// Initialize the logger system.
|
||||
LoggerModule.Initialize();
|
||||
|
||||
// Register mime types on linux.
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
RegisterMimeTypes();
|
||||
}
|
||||
|
||||
// Initialize Discord integration.
|
||||
DiscordIntegrationModule.Initialize();
|
||||
|
||||
@@ -218,4 +268,4 @@ namespace Ryujinx.Ava
|
||||
Logger.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -940,7 +940,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public static void OpenLogsFolder()
|
||||
{
|
||||
string logPath = Path.Combine(ReleaseInformations.GetBaseApplicationDirectory(), "Logs");
|
||||
string logPath = Path.Combine(ReleaseInformation.GetBaseApplicationDirectory(), "Logs");
|
||||
|
||||
new DirectoryInfo(logPath).Create();
|
||||
|
||||
@@ -1018,7 +1018,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public async void OpenAboutWindow()
|
||||
{
|
||||
await new AboutWindow().ShowDialog(_owner);
|
||||
await AboutWindow.Show();
|
||||
}
|
||||
|
||||
public void ChangeLanguage(object obj)
|
||||
|
@@ -1,4 +1,3 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
@@ -8,8 +7,6 @@ using Ryujinx.Audio.Backends.OpenAL;
|
||||
using Ryujinx.Audio.Backends.SDL2;
|
||||
using Ryujinx.Audio.Backends.SoundIo;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Common.Configuration;
|
||||
@@ -19,7 +16,6 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.Vulkan;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS.Services.Time.TimeZone;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Ui.Common.Configuration;
|
||||
using Ryujinx.Ui.Common.Configuration.System;
|
||||
using System;
|
||||
@@ -30,11 +26,10 @@ using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
internal class SettingsViewModel : BaseModel
|
||||
public class SettingsViewModel : BaseModel
|
||||
{
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly ContentManager _contentManager;
|
||||
private readonly StyleableWindow _owner;
|
||||
private TimeZoneContentManager _timeZoneContentManager;
|
||||
|
||||
private readonly List<string> _validTzRegions;
|
||||
@@ -44,10 +39,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private int _graphicsBackendMultithreadingIndex;
|
||||
private float _volume;
|
||||
private bool _isVulkanAvailable = true;
|
||||
private bool _directoryChanged = false;
|
||||
private List<string> _gpuIds = new List<string>();
|
||||
private bool _directoryChanged;
|
||||
private List<string> _gpuIds = new();
|
||||
private KeyboardHotkeys _keyboardHotkeys;
|
||||
private int _graphicsBackendIndex;
|
||||
private string _customThemePath;
|
||||
|
||||
public event Action CloseWindow;
|
||||
public event Action SaveSettingsEvent;
|
||||
|
||||
public int ResolutionScale
|
||||
{
|
||||
@@ -67,19 +66,16 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
_graphicsBackendMultithreadingIndex = value;
|
||||
|
||||
if (_owner != null)
|
||||
if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value)
|
||||
{
|
||||
if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value)
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(async () =>
|
||||
{
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage],
|
||||
"",
|
||||
"",
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||
LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle]);
|
||||
});
|
||||
}
|
||||
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage],
|
||||
"",
|
||||
"",
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||
LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle]);
|
||||
});
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
@@ -120,12 +116,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool IsMacOS
|
||||
{
|
||||
get => OperatingSystem.IsMacOS();
|
||||
}
|
||||
|
||||
|
||||
public bool EnableDiscordIntegration { get; set; }
|
||||
public bool CheckUpdatesOnStart { get; set; }
|
||||
public bool ShowConfirmExit { get; set; }
|
||||
@@ -160,7 +156,20 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public string TimeZone { get; set; }
|
||||
public string ShaderDumpPath { get; set; }
|
||||
public string CustomThemePath { get; set; }
|
||||
|
||||
public string CustomThemePath
|
||||
{
|
||||
get
|
||||
{
|
||||
return _customThemePath;
|
||||
}
|
||||
set
|
||||
{
|
||||
_customThemePath = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int Language { get; set; }
|
||||
public int Region { get; set; }
|
||||
@@ -191,7 +200,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
_volume = value;
|
||||
|
||||
ConfigurationState.Instance.System.AudioVolume.Value = (float)(_volume / 100);
|
||||
ConfigurationState.Instance.System.AudioVolume.Value = _volume / 100;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
@@ -199,7 +208,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public DateTimeOffset DateOffset { get; set; }
|
||||
public TimeSpan TimeOffset { get; set; }
|
||||
public AvaloniaList<TimeZone> TimeZones { get; set; }
|
||||
private AvaloniaList<TimeZone> TimeZones { get; set; }
|
||||
public AvaloniaList<string> GameDirectories { get; set; }
|
||||
public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
|
||||
|
||||
@@ -214,17 +223,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public IGamepadDriver AvaloniaKeyboardDriver { get; }
|
||||
|
||||
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager, StyleableWindow owner) : this()
|
||||
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
|
||||
{
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_contentManager = contentManager;
|
||||
_owner = owner;
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
LoadTimeZones();
|
||||
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,10 +256,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported;
|
||||
}
|
||||
|
||||
private unsafe void LoadAvailableGpus()
|
||||
private void LoadAvailableGpus()
|
||||
{
|
||||
_gpuIds = new List<string>();
|
||||
List<string> names = new List<string>();
|
||||
List<string> names = new();
|
||||
var devices = VulkanRenderer.GetPhysicalDevices();
|
||||
|
||||
if (devices.Length == 0)
|
||||
@@ -272,7 +277,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
|
||||
AvailableGpus.Clear();
|
||||
AvailableGpus.AddRange(names.Select(x => new ComboBoxItem() { Content = x }));
|
||||
AvailableGpus.AddRange(names.Select(x => new ComboBoxItem { Content = x }));
|
||||
}
|
||||
|
||||
public void LoadTimeZones()
|
||||
@@ -302,25 +307,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public async void BrowseTheme()
|
||||
{
|
||||
var dialog = new OpenFileDialog()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SettingsSelectThemeFileDialogTitle],
|
||||
AllowMultiple = false
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter() { Extensions = { "xaml" }, Name = LocaleManager.Instance[LocaleKeys.SettingsXamlThemeFile] });
|
||||
|
||||
var file = await dialog.ShowAsync(_owner);
|
||||
|
||||
if (file != null && file.Length > 0)
|
||||
{
|
||||
CustomThemePath = file[0];
|
||||
OnPropertyChanged(nameof(CustomThemePath));
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadCurrentConfiguration()
|
||||
{
|
||||
ConfigurationState config = ConfigurationState.Instance;
|
||||
@@ -477,16 +463,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
|
||||
MainWindow.UpdateGraphicsConfig();
|
||||
|
||||
if (_owner is SettingsWindow owner)
|
||||
{
|
||||
owner.ControllerSettings?.SaveCurrentProfile();
|
||||
}
|
||||
|
||||
if (_owner.Owner is MainWindow window && _directoryChanged)
|
||||
{
|
||||
window.ViewModel.LoadApplications();
|
||||
}
|
||||
|
||||
SaveSettingsEvent?.Invoke();
|
||||
|
||||
_directoryChanged = false;
|
||||
}
|
||||
@@ -504,13 +482,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
public void OkButton()
|
||||
{
|
||||
SaveSettings();
|
||||
_owner.Close();
|
||||
CloseWindow?.Invoke();
|
||||
}
|
||||
|
||||
public void CancelButton()
|
||||
{
|
||||
RevertIfNotSaved();
|
||||
_owner.Close();
|
||||
CloseWindow?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
81
Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml
Normal file
81
Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml
Normal file
@@ -0,0 +1,81 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsAudioView"
|
||||
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<ScrollViewer
|
||||
Name="AudioPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabAudio}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemAudioBackend}"
|
||||
ToolTip.Tip="{locale:Locale AudioBackendTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding AudioBackend}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendDummy}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsOpenAlEnabled}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendOpenAL}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsSoundIoEnabled}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSoundIO}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsSDL2Enabled}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSDL2}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemAudioVolume}"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Width="250" />
|
||||
<ui:NumberBox Value="{Binding Volume}"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Width="350"
|
||||
SmallChange="1"
|
||||
LargeChange="10"
|
||||
SimpleNumberFormat="F0"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Minimum="0"
|
||||
Maximum="100" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<Slider Value="{Binding Volume}"
|
||||
Margin="250,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
SmallChange="5"
|
||||
TickFrequency="5"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="10"
|
||||
Width="350" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
12
Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml.cs
Normal file
12
Ryujinx.Ava/UI/Views/Settings/SettingsAudioView.axaml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsAudioView : UserControl
|
||||
{
|
||||
public SettingsAudioView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
72
Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml
Normal file
72
Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml
Normal file
@@ -0,0 +1,72 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsCPUView"
|
||||
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:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<ScrollViewer
|
||||
Name="CpuPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuCache}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnablePptc}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnablePptc}"
|
||||
ToolTip.Tip="{locale:Locale PptcToggleTooltip}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuMemory}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemMemoryManagerMode}"
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding MemoryMode}"
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerSoftwareTooltip}">
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemMemoryManagerModeSoftware}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerHostTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemMemoryManagerModeHost}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerUnsafeTooltip}">
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemMemoryManagerModeHostUnchecked}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
12
Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml.cs
Normal file
12
Ryujinx.Ava/UI/Views/Settings/SettingsCPUView.axaml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsCPUView : UserControl
|
||||
{
|
||||
public SettingsCPUView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
218
Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml
Normal file
218
Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml
Normal file
@@ -0,0 +1,218 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsGraphicsView"
|
||||
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<ScrollViewer
|
||||
Name="GraphicsPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsAPI}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsBackend}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
|
||||
SelectedIndex="{Binding GraphicsBackendIndex}">
|
||||
<ComboBoxItem IsVisible="{Binding IsVulkanAvailable}">
|
||||
<TextBlock Text="Vulkan" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
|
||||
<TextBlock Text="OpenGL" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsPreferredGpu}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
|
||||
SelectedIndex="{Binding PreferredGpuIndex}"
|
||||
Items="{Binding AvailableGpus}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsFeatures}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableShaderCache}"
|
||||
ToolTip.Tip="{locale:Locale ShaderCacheToggleTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsEnableShaderCache}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableTextureRecompression}"
|
||||
ToolTip.Tip="{locale:Locale SettingsEnableTextureRecompressionTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsEnableTextureRecompression}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableMacroHLE}"
|
||||
ToolTip.Tip="{locale:Locale SettingsEnableMacroHLETooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsResolutionScale}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding ResolutionScale}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleCustom}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleNative}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale2x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale3x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale4x}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<ui:NumberBox
|
||||
Margin="10,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale ResolutionScaleEntryTooltip}"
|
||||
MinWidth="150"
|
||||
SmallChange="0.1"
|
||||
LargeChange="1"
|
||||
SimpleNumberFormat="F2"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
IsVisible="{Binding IsCustomResolutionScaleActive}"
|
||||
Maximum="100"
|
||||
Minimum="0.1"
|
||||
Value="{Binding CustomResolutionScale}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale AnisotropyTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding MaxAnisotropy}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale AnisotropyTooltip}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale AspectRatioTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsAspectRatio}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding AspectRatio}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale AspectRatioTooltip}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio4x3}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x9}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x10}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio21x9}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio32x9}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatioStretch}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale GraphicsBackendThreadingTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsBackendMultithreading}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale GalThreadingTooltip}"
|
||||
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale CommonAuto}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale CommonOff}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale CommonOn}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsDeveloperOptions}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsShaderDumpPath}"
|
||||
Width="250" />
|
||||
<TextBox Text="{Binding ShaderDumpPath}"
|
||||
Width="350"
|
||||
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
12
Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml.cs
Normal file
12
Ryujinx.Ava/UI/Views/Settings/SettingsGraphicsView.axaml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsGraphicsView : UserControl
|
||||
{
|
||||
public SettingsGraphicsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
104
Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml
Normal file
104
Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml
Normal file
@@ -0,0 +1,104 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsHotkeysView"
|
||||
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:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel"
|
||||
Focusable="True">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<UserControl.Resources>
|
||||
<helpers:KeyValueConverter x:Key="Key" />
|
||||
</UserControl.Resources>
|
||||
<ScrollViewer
|
||||
Name="HotkeysPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel Margin="10" Orientation="Vertical" Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabHotkeysHotkeys}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ToggleVsync, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.Screenshot, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ShowUi, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.Pause, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ToggleMute, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ResScaleUp, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ResScaleDown, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.VolumeUp, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.VolumeDown, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
81
Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml.cs
Normal file
81
Ryujinx.Ava/UI/Views/Settings/SettingsHotkeysView.axaml.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.Assigner;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsHotkeysView : UserControl
|
||||
{
|
||||
private ButtonKeyAssigner _currentAssigner;
|
||||
private IGamepadDriver AvaloniaKeyboardDriver;
|
||||
|
||||
public SettingsHotkeysView()
|
||||
{
|
||||
InitializeComponent();
|
||||
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
|
||||
}
|
||||
|
||||
private void MouseClick(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
bool shouldUnbind = e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed;
|
||||
|
||||
_currentAssigner?.Cancel(shouldUnbind);
|
||||
|
||||
PointerPressed -= MouseClick;
|
||||
}
|
||||
|
||||
private void Button_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleButton button)
|
||||
{
|
||||
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentAssigner == null && button.IsChecked != null && (bool)button.IsChecked)
|
||||
{
|
||||
_currentAssigner = new ButtonKeyAssigner(button);
|
||||
|
||||
FocusManager.Instance?.Focus(this, NavigationMethod.Pointer);
|
||||
|
||||
PointerPressed += MouseClick;
|
||||
|
||||
var keyboard = (IKeyboard)AvaloniaKeyboardDriver.GetGamepad(AvaloniaKeyboardDriver.GamepadsIds[0]);
|
||||
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
|
||||
|
||||
_currentAssigner.GetInputAndAssign(assigner);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_currentAssigner != null)
|
||||
{
|
||||
ToggleButton oldButton = _currentAssigner.ToggledButton;
|
||||
|
||||
_currentAssigner.Cancel();
|
||||
_currentAssigner = null;
|
||||
|
||||
button.IsChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
}
|
||||
}
|
||||
}
|
46
Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml
Normal file
46
Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml
Normal file
@@ -0,0 +1,46 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsInputView"
|
||||
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:window="clr-namespace:Ryujinx.Ava.UI.Windows"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<ScrollViewer
|
||||
Name="InputPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel Margin="4" Orientation="Vertical">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<CheckBox Margin="5,0"
|
||||
ToolTip.Tip="{locale:Locale DockModeToggleTooltip}"
|
||||
IsChecked="{Binding EnableDockedMode}">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabInputEnableDockedMode}" />
|
||||
</CheckBox>
|
||||
<CheckBox Margin="5,0"
|
||||
ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}"
|
||||
IsChecked="{Binding EnableKeyboard}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" />
|
||||
</CheckBox>
|
||||
<CheckBox Margin="5,0"
|
||||
ToolTip.Tip="{locale:Locale DirectMouseTooltip}"
|
||||
IsChecked="{Binding EnableMouse}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabInputDirectMouseAccess}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<window:ControllerSettingsWindow Name="ControllerSettings" Margin="0" MinHeight="600" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
17
Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs
Normal file
17
Ryujinx.Ava/UI/Views/Settings/SettingsInputView.axaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsInputView : UserControl
|
||||
{
|
||||
public SettingsInputView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ControllerSettings.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
118
Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml
Normal file
118
Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml
Normal file
@@ -0,0 +1,118 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsLoggingView"
|
||||
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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<ScrollViewer
|
||||
Name="LoggingPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingLogging}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableFileLog}"
|
||||
ToolTip.Tip="{locale:Locale FileLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableLoggingToFile}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableStub}"
|
||||
ToolTip.Tip="{locale:Locale StubLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableStubLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableInfo}"
|
||||
ToolTip.Tip="{locale:Locale InfoLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableInfoLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableWarn}"
|
||||
ToolTip.Tip="{locale:Locale WarnLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableWarningLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableError}"
|
||||
ToolTip.Tip="{locale:Locale ErrorLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableErrorLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableTrace}"
|
||||
ToolTip.Tip="{locale:Locale TraceLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableTraceLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableGuest}"
|
||||
ToolTip.Tip="{locale:Locale GuestLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableGuestLogs}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingDeveloperOptions}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableDebug}"
|
||||
ToolTip.Tip="{locale:Locale DebugLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableDebugLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableFsAccessLog}"
|
||||
ToolTip.Tip="{locale:Locale FileAccessLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableFsAccessLogs}" />
|
||||
</CheckBox>
|
||||
<StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale FSAccessLogModeTooltip}"
|
||||
Text="{locale:Locale SettingsTabLoggingFsGlobalAccessLogMode}"
|
||||
Width="285" />
|
||||
<ui:NumberBox
|
||||
Maximum="3"
|
||||
Minimum="0"
|
||||
Width="150"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
SmallChange="1"
|
||||
LargeChange="1"
|
||||
Value="{Binding FsGlobalAccessLogMode}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,10,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevel}"
|
||||
ToolTip.Tip="{locale:Locale OpenGlLogLevel}"
|
||||
Width="285" />
|
||||
<ComboBox SelectedIndex="{Binding OpenglDebugLevel}"
|
||||
Width="150"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale OpenGlLogLevel}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelNone}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelError}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelPerformance}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelAll}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
12
Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml.cs
Normal file
12
Ryujinx.Ava/UI/Views/Settings/SettingsLoggingView.axaml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsLoggingView : UserControl
|
||||
{
|
||||
public SettingsLoggingView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
35
Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml
Normal file
35
Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml
Normal file
@@ -0,0 +1,35 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsNetworkView"
|
||||
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:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<ScrollViewer
|
||||
Name="NetworkPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkConnection}" />
|
||||
<CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnableInternetAccess}"
|
||||
ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
12
Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml.cs
Normal file
12
Ryujinx.Ava/UI/Views/Settings/SettingsNetworkView.axaml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsNetworkView : UserControl
|
||||
{
|
||||
public SettingsNetworkView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
195
Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml
Normal file
195
Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml
Normal file
@@ -0,0 +1,195 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsSystemView"
|
||||
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:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<ScrollViewer
|
||||
Name="SystemPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemCore}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemRegion}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding Region}"
|
||||
ToolTip.Tip="{locale:Locale RegionTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionJapan}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionUSA}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionEurope}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionAustralia}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionChina}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionKorea}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionTaiwan}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguage}"
|
||||
ToolTip.Tip="{locale:Locale LanguageTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding Language}"
|
||||
ToolTip.Tip="{locale:Locale LanguageTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageJapanese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageAmericanEnglish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageFrench}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageGerman}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageItalian}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageSpanish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageChinese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageKorean}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageDutch}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguagePortuguese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageRussian}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageTaiwanese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageBritishEnglish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageCanadianFrench}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageLatinAmericanSpanish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageSimplifiedChinese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageTraditionalChinese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageBrazilianPortuguese}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemTimeZone}"
|
||||
ToolTip.Tip="{locale:Locale TimezoneTooltip}"
|
||||
Width="250" />
|
||||
<AutoCompleteBox
|
||||
Name="TimeZoneBox"
|
||||
Width="350"
|
||||
MaxDropDownHeight="500"
|
||||
FilterMode="Contains"
|
||||
Items="{Binding TimeZones}"
|
||||
SelectionChanged="TimeZoneBox_OnSelectionChanged"
|
||||
Text="{Binding Path=TimeZone, Mode=OneWay}"
|
||||
TextChanged="TimeZoneBox_OnTextChanged"
|
||||
ToolTip.Tip="{locale:Locale TimezoneTooltip}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemTime}"
|
||||
ToolTip.Tip="{locale:Locale TimeTooltip}"
|
||||
Width="250"/>
|
||||
<DatePicker VerticalAlignment="Center" SelectedDate="{Binding DateOffset}"
|
||||
ToolTip.Tip="{locale:Locale TimeTooltip}"
|
||||
Width="350" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="250,0,0,10" Orientation="Horizontal">
|
||||
<TimePicker
|
||||
VerticalAlignment="Center"
|
||||
ClockIdentifier="24HourClock"
|
||||
SelectedTime="{Binding TimeOffset}"
|
||||
Width="350"
|
||||
ToolTip.Tip="{locale:Locale TimeTooltip}" />
|
||||
</StackPanel>
|
||||
<CheckBox IsChecked="{Binding EnableVsync}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnableVsync}"
|
||||
ToolTip.Tip="{locale:Locale VSyncToggleTooltip}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableFsIntegrityChecks}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnableFsIntegrityChecks}"
|
||||
ToolTip.Tip="{locale:Locale FsIntegrityToggleTooltip}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemHacks}" />
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemHacksNote}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding ExpandDramSize}"
|
||||
ToolTip.Tip="{locale:Locale DRamTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemExpandDramSize}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding IgnoreMissingServices}"
|
||||
ToolTip.Tip="{locale:Locale IgnoreMissingServicesTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemIgnoreMissingServices}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
52
Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml.cs
Normal file
52
Ryujinx.Ava/UI/Views/Settings/SettingsSystemView.axaml.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsSystemView : UserControl
|
||||
{
|
||||
public SettingsViewModel ViewModel;
|
||||
|
||||
public SettingsSystemView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()).Trim());
|
||||
MultiBinding tzMultiBinding = new() { Converter = converter };
|
||||
|
||||
tzMultiBinding.Bindings.Add(new Binding("UtcDifference"));
|
||||
tzMultiBinding.Bindings.Add(new Binding("Location"));
|
||||
tzMultiBinding.Bindings.Add(new Binding("Abbreviation"));
|
||||
|
||||
TimeZoneBox.ValueMemberBinding = tzMultiBinding;
|
||||
}
|
||||
|
||||
private void TimeZoneBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.AddedItems != null && e.AddedItems.Count > 0)
|
||||
{
|
||||
if (e.AddedItems[0] is TimeZone timeZone)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TimeZoneBox_OnTextChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is AutoCompleteBox box && box.SelectedItem is TimeZone timeZone)
|
||||
{
|
||||
{
|
||||
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
156
Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml
Normal file
156
Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml
Normal file
@@ -0,0 +1,156 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsUIView"
|
||||
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:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="viewModels:SettingsViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<ScrollViewer
|
||||
Name="UiPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGeneral}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableDiscordIntegration}">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale ToggleDiscordTooltip}"
|
||||
Text="{locale:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding CheckUpdatesOnStart}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralCheckUpdatesOnLaunch}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding ShowConfirmExit}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralShowConfirmExitDialog}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding HideCursorOnIdle}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGameDirectories}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<ListBox
|
||||
Name="GameList"
|
||||
MinHeight="230"
|
||||
Items="{Binding GameDirectories}">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
</ListBox>
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Name="PathBox"
|
||||
Margin="0"
|
||||
ToolTip.Tip="{locale:Locale AddGameDirBoxTooltip}"
|
||||
VerticalAlignment="Stretch" />
|
||||
<Button
|
||||
Name="AddButton"
|
||||
Grid.Column="1"
|
||||
MinWidth="90"
|
||||
Margin="10,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale AddGameDirTooltip}"
|
||||
Click="AddButton_OnClick">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveButton"
|
||||
Grid.Column="2"
|
||||
MinWidth="90"
|
||||
Margin="10,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale RemoveGameDirTooltip}"
|
||||
Click="RemoveButton_OnClick">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralTheme}" />
|
||||
<Grid Margin="10,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<CheckBox
|
||||
IsChecked="{Binding EnableCustomTheme}"
|
||||
ToolTip.Tip="{locale:Locale CustomThemeCheckTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeEnableCustomTheme}" />
|
||||
</CheckBox>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
Text="{locale:Locale SettingsTabGeneralThemeCustomTheme}"
|
||||
ToolTip.Tip="{locale:Locale CustomThemePathTooltip}" />
|
||||
<TextBox
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="0,10,0,0"
|
||||
Text="{Binding CustomThemePath}" />
|
||||
<Button
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="10,10,0,0"
|
||||
Click="BrowseTheme"
|
||||
ToolTip.Tip="{locale:Locale CustomThemeBrowseTooltip}"
|
||||
Content="{locale:Locale ButtonBrowse}" />
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
Text="{locale:Locale SettingsTabGeneralThemeBaseStyle}" />
|
||||
<ComboBox
|
||||
Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
MinWidth="100"
|
||||
SelectedIndex="{Binding BaseStyleIndex}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleLight}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleDark}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
82
Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml.cs
Normal file
82
Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Settings
|
||||
{
|
||||
public partial class SettingsUIView : UserControl
|
||||
{
|
||||
public SettingsViewModel ViewModel;
|
||||
|
||||
public SettingsUIView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private async void AddButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
string path = PathBox.Text;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path))
|
||||
{
|
||||
ViewModel.GameDirectories.Add(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
path = await new OpenFolderDialog().ShowAsync(desktop.MainWindow);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
ViewModel.GameDirectories.Add(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
int oldIndex = GameList.SelectedIndex;
|
||||
|
||||
foreach (string path in new List<string>(GameList.SelectedItems.Cast<string>()))
|
||||
{
|
||||
ViewModel.GameDirectories.Remove(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
}
|
||||
|
||||
if (GameList.ItemCount > 0)
|
||||
{
|
||||
GameList.SelectedIndex = oldIndex < GameList.ItemCount ? oldIndex : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async void BrowseTheme(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new OpenFileDialog()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.SettingsSelectThemeFileDialogTitle],
|
||||
AllowMultiple = false
|
||||
};
|
||||
|
||||
dialog.Filters.Add(new FileDialogFilter() { Extensions = { "xaml" }, Name = LocaleManager.Instance[LocaleKeys.SettingsXamlThemeFile] });
|
||||
|
||||
if (Avalonia.Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var file = await dialog.ShowAsync(desktop.MainWindow);
|
||||
|
||||
if (file != null && file.Length > 0)
|
||||
{
|
||||
ViewModel.CustomThemePath = file[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,282 +1,253 @@
|
||||
<window:StyleableWindow
|
||||
x:Class="Ryujinx.Ava.UI.Windows.AboutWindow"
|
||||
<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:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:flex="clr-namespace:Avalonia.Flexbox;assembly=Avalonia.Flexbox"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:viewModel="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
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"
|
||||
d:DesignHeight="260"
|
||||
d:DesignWidth="550"
|
||||
Height="260"
|
||||
Width="550"
|
||||
x:Class="Ryujinx.Ava.UI.Windows.AboutWindow"
|
||||
x:DataType="viewModel:AboutWindowViewModel"
|
||||
x:CompileBindings="True"
|
||||
Margin="0 -12 0 0"
|
||||
Focusable="True">
|
||||
<Design.DataContext>
|
||||
<viewModel:AboutWindowViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid
|
||||
Margin="15"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<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">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</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"
|
||||
Source="resm:Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png?assembly=Ryujinx.Ui.Common" />
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="0,20,0,0"
|
||||
FontSize="35"
|
||||
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>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Version}"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
Grid.Row="2"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Center"
|
||||
MaxLines="2"
|
||||
Text="{locale:Locale AboutDisclaimerMessage}"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
Name="AmiiboLabel"
|
||||
Grid.Row="3"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Center"
|
||||
MaxLines="2"
|
||||
PointerPressed="AmiiboLabel_OnPointerPressed"
|
||||
Text="{locale:Locale AboutAmiiboDisclaimerMessage}"
|
||||
TextAlignment="Center" />
|
||||
<StackPanel
|
||||
Grid.Row="4"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Vertical" ToolTip.Tip="{locale:Locale AboutPatreonUrlTooltipMessage}">
|
||||
Grid.Row="0"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image
|
||||
Grid.Column="0"
|
||||
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
|
||||
FontSize="28"
|
||||
FontWeight="Bold"
|
||||
Text="Ryujinx"
|
||||
TextAlignment="Left" />
|
||||
<TextBlock
|
||||
Text="(REE-YOU-JINX)"
|
||||
TextAlignment="Left" />
|
||||
</flex:FlexPanel>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding Version}"
|
||||
TextAlignment="Center"
|
||||
FontSize="10"
|
||||
LineHeight="12" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Width="200"
|
||||
Text="{locale:Locale AboutDisclaimerMessage}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="10"
|
||||
LineHeight="12" />
|
||||
<TextBlock
|
||||
Name="AmiiboLabel"
|
||||
HorizontalAlignment="Center"
|
||||
Width="200"
|
||||
PointerPressed="AmiiboLabel_OnPointerPressed"
|
||||
Text="{locale:Locale AboutAmiiboDisclaimerMessage}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="10"
|
||||
LineHeight="12" />
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<Button
|
||||
Height="65"
|
||||
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>
|
||||
MaxHeight="30"
|
||||
MaxWidth="30"
|
||||
MinHeight="30"
|
||||
MinWidth="30"
|
||||
Padding="8"
|
||||
CornerRadius="15"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
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"
|
||||
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>
|
||||
MaxHeight="30"
|
||||
MaxWidth="30"
|
||||
MinHeight="30"
|
||||
MinWidth="30"
|
||||
Padding="8"
|
||||
CornerRadius="15"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
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"
|
||||
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>
|
||||
MaxHeight="30"
|
||||
MaxWidth="30"
|
||||
MinHeight="30"
|
||||
MinWidth="30"
|
||||
Padding="8"
|
||||
CornerRadius="15"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
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"
|
||||
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>
|
||||
MaxHeight="30"
|
||||
MaxWidth="30"
|
||||
MinHeight="30"
|
||||
MinWidth="30"
|
||||
Padding="8"
|
||||
CornerRadius="15"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://twitter.com/RyujinxEmu"
|
||||
ToolTip.Tip="{locale:Locale AboutTwitterUrlTooltipMessage}">
|
||||
<Image Source="{Binding TwitterLogo}" />
|
||||
</Button>
|
||||
<Button
|
||||
MaxHeight="30"
|
||||
MaxWidth="30"
|
||||
MinHeight="30"
|
||||
MinWidth="30"
|
||||
Padding="8"
|
||||
CornerRadius="15"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://www.ryujinx.org"
|
||||
ToolTip.Tip="{locale:Locale AboutUrlTooltipMessage}">
|
||||
<ui:SymbolIcon
|
||||
Symbol="Link"
|
||||
Foreground="{DynamicResource ThemeForegroundColor}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Width="2"
|
||||
Margin="5"
|
||||
Width="1"
|
||||
VerticalAlignment="Stretch"
|
||||
BorderBrush="White"
|
||||
BorderThickness="1,0,0,0">
|
||||
<Separator Width="0" />
|
||||
</Border>
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1,0,0,0"
|
||||
Margin="20 0"/>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="20"
|
||||
HorizontalAlignment="Stretch"
|
||||
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"
|
||||
Margin="0,10,0,0"
|
||||
FontWeight="Bold"
|
||||
Text="{locale:Locale AboutRyujinxMaintainersTitle}"
|
||||
TextDecorations="Underline" />
|
||||
<TextBlock
|
||||
Grid.Row="3"
|
||||
Margin="20,5,5,5"
|
||||
LineHeight="20"
|
||||
Text="{Binding Developers}" />
|
||||
<Button
|
||||
Grid.Row="4"
|
||||
HorizontalAlignment="Right"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Tag="https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a">
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="0 10 0 0"
|
||||
Spacing="2">
|
||||
<TextBlock
|
||||
Text="{locale:Locale AboutRyujinxContributorsButtonHeader}"
|
||||
TextAlignment="Right"
|
||||
TextDecorations="Underline"
|
||||
ToolTip.Tip="{locale:Locale AboutRyujinxMaintainersContentTooltipMessage}" />
|
||||
</Button>
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Margin="0,0,0,0"
|
||||
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">
|
||||
FontWeight="Bold"
|
||||
FontSize="15"
|
||||
Text="{locale:Locale AboutRyujinxAboutTitle}" />
|
||||
<TextBlock
|
||||
Name="SupportersTextBlock"
|
||||
VerticalAlignment="Top"
|
||||
Text="{Binding Supporters}"
|
||||
TextWrapping="Wrap" />
|
||||
</Border>
|
||||
FontSize="10"
|
||||
TextWrapping="Wrap"
|
||||
Text="{locale:Locale AboutRyujinxAboutContent}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0 10 0 0"
|
||||
Spacing="2">
|
||||
<TextBlock
|
||||
FontWeight="Bold"
|
||||
FontSize="15"
|
||||
Text="{locale:Locale AboutRyujinxMaintainersTitle}" />
|
||||
<TextBlock
|
||||
FontSize="10"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding Developers}" />
|
||||
<Button
|
||||
HorizontalAlignment="Left"
|
||||
Background="Transparent"
|
||||
Click="Button_OnClick"
|
||||
Padding="5"
|
||||
Tag="https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a">
|
||||
<TextBlock
|
||||
FontSize="10"
|
||||
Text="{locale:Locale AboutRyujinxContributorsButtonHeader}"
|
||||
TextAlignment="Right"
|
||||
ToolTip.Tip="{locale:Locale AboutRyujinxMaintainersContentTooltipMessage}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0 10 0 0"
|
||||
Spacing="2">
|
||||
<TextBlock
|
||||
FontWeight="Bold"
|
||||
FontSize="15"
|
||||
Text="{locale:Locale AboutRyujinxSupprtersTitle}" />
|
||||
<ScrollViewer
|
||||
VerticalScrollBarVisibility="Visible"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Height="70">
|
||||
<TextBlock
|
||||
Name="SupportersTextBlock"
|
||||
VerticalAlignment="Top"
|
||||
FontSize="10"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding Supporters}" />
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</window:StyleableWindow>
|
||||
</UserControl>
|
@@ -1,38 +1,48 @@
|
||||
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.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()
|
||||
{
|
||||
var 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«");
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
PrimaryButtonText = "",
|
||||
SecondaryButtonText = "",
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
|
||||
Content = content
|
||||
};
|
||||
|
||||
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 contentDialog.ShowAsync();
|
||||
}
|
||||
|
||||
private void Button_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
@@ -42,31 +52,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)
|
||||
|
@@ -343,6 +343,7 @@
|
||||
Margin="5,0,5,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
DockPanel.Dock="Right"
|
||||
KeyUp="SearchBox_OnKeyUp"
|
||||
Text="{Binding SearchText}"
|
||||
|
@@ -6,13 +6,12 @@
|
||||
xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
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:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
Width="1100"
|
||||
Height="768"
|
||||
d:DesignWidth="800"
|
||||
d:DesignHeight="950"
|
||||
MinWidth="800"
|
||||
MinHeight="480"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
@@ -23,9 +22,6 @@
|
||||
<Design.DataContext>
|
||||
<viewModels:SettingsViewModel />
|
||||
</Design.DataContext>
|
||||
<Window.Resources>
|
||||
<helpers:KeyValueConverter x:Key="Key" />
|
||||
</Window.Resources>
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinWidth="600">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -38,871 +34,15 @@
|
||||
IsVisible="False"
|
||||
KeyboardNavigation.IsTabStop="False"/>
|
||||
<Grid Name="Pages" IsVisible="False" Grid.Row="2">
|
||||
<ScrollViewer Name="UiPage"
|
||||
Margin="0,0,2,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGeneral}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableDiscordIntegration}">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale ToggleDiscordTooltip}"
|
||||
Text="{locale:Locale SettingsTabGeneralEnableDiscordRichPresence}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding CheckUpdatesOnStart}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralCheckUpdatesOnLaunch}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding ShowConfirmExit}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralShowConfirmExitDialog}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding HideCursorOnIdle}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGameDirectories}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<ListBox
|
||||
Name="GameList"
|
||||
MinHeight="250"
|
||||
Items="{Binding GameDirectories}" />
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Name="PathBox"
|
||||
Margin="0"
|
||||
ToolTip.Tip="{locale:Locale AddGameDirBoxTooltip}"
|
||||
VerticalAlignment="Stretch" />
|
||||
<Button
|
||||
Name="AddButton"
|
||||
Grid.Column="1"
|
||||
MinWidth="90"
|
||||
Margin="10,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale AddGameDirTooltip}"
|
||||
Click="AddButton_OnClick">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabGeneralAdd}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="RemoveButton"
|
||||
Grid.Column="2"
|
||||
MinWidth="90"
|
||||
Margin="10,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale RemoveGameDirTooltip}"
|
||||
Click="RemoveButton_OnClick">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabGeneralRemove}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralTheme}" />
|
||||
<Grid Margin="10,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<CheckBox IsChecked="{Binding EnableCustomTheme}"
|
||||
ToolTip.Tip="{locale:Locale CustomThemeCheckTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeEnableCustomTheme}" />
|
||||
</CheckBox>
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
Grid.Row="1"
|
||||
Text="{locale:Locale SettingsTabGeneralThemeCustomTheme}"
|
||||
ToolTip.Tip="{locale:Locale CustomThemePathTooltip}" />
|
||||
<TextBox Margin="0,10,0,0"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Text="{Binding CustomThemePath}" />
|
||||
<Button Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
Margin="10,10,0,0"
|
||||
Command="{ReflectionBinding BrowseTheme}"
|
||||
ToolTip.Tip="{locale:Locale CustomThemeBrowseTooltip}"
|
||||
Content="{locale:Locale ButtonBrowse}" />
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
Grid.Row="2"
|
||||
Text="{locale:Locale SettingsTabGeneralThemeBaseStyle}" />
|
||||
<ComboBox VerticalAlignment="Center"
|
||||
Margin="0,10,0,0"
|
||||
Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
MinWidth="100"
|
||||
SelectedIndex="{Binding BaseStyleIndex}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleLight}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGeneralThemeBaseStyleDark}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer Name="InputPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Padding="0,0,2,0"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel Margin="4" Orientation="Vertical">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<CheckBox Margin="5,0"
|
||||
ToolTip.Tip="{locale:Locale DockModeToggleTooltip}"
|
||||
IsChecked="{Binding EnableDockedMode}">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabInputEnableDockedMode}" />
|
||||
</CheckBox>
|
||||
<CheckBox Margin="5,0"
|
||||
ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}"
|
||||
IsChecked="{Binding EnableKeyboard}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" />
|
||||
</CheckBox>
|
||||
<CheckBox Margin="5,0"
|
||||
ToolTip.Tip="{locale:Locale DirectMouseTooltip}"
|
||||
IsChecked="{Binding EnableMouse}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabInputDirectMouseAccess}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<window:ControllerSettingsWindow Name="ControllerSettings" Margin="0,0,0,0" MinHeight="600" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer Name="HotkeysPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel Margin="10,5" Orientation="Vertical" Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabHotkeysHotkeys}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleVsyncHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ToggleVsync, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysScreenshotHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.Screenshot, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysShowUiHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ShowUi, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysPauseHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.Pause, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysToggleMuteHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ToggleMute, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleUpHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ResScaleUp, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysResScaleDownHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.ResScaleDown, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeUpHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.VolumeUp, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center" Text="{locale:Locale SettingsTabHotkeysVolumeDownHotkey}" Width="230" />
|
||||
<ToggleButton Width="90" Height="27" Checked="Button_Checked" Unchecked="Button_Unchecked">
|
||||
<TextBlock
|
||||
Text="{Binding KeyboardHotkeys.VolumeDown, Mode=TwoWay, Converter={StaticResource Key}}"
|
||||
TextAlignment="Center" />
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer Name="SystemPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemCore}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemRegion}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding Region}"
|
||||
ToolTip.Tip="{locale:Locale RegionTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionJapan}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionUSA}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionEurope}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionAustralia}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionChina}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionKorea}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemRegionTaiwan}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguage}"
|
||||
ToolTip.Tip="{locale:Locale LanguageTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding Language}"
|
||||
ToolTip.Tip="{locale:Locale LanguageTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageJapanese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageAmericanEnglish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageFrench}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageGerman}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageItalian}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageSpanish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageChinese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageKorean}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageDutch}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguagePortuguese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageRussian}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemSystemLanguageTaiwanese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageBritishEnglish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageCanadianFrench}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageLatinAmericanSpanish}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageSimplifiedChinese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageTraditionalChinese}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemSystemLanguageBrazilianPortuguese}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemTimeZone}"
|
||||
ToolTip.Tip="{locale:Locale TimezoneTooltip}"
|
||||
Width="250" />
|
||||
<AutoCompleteBox
|
||||
Name="TimeZoneBox"
|
||||
Width="350"
|
||||
MaxDropDownHeight="500"
|
||||
FilterMode="Contains"
|
||||
Items="{Binding TimeZones}"
|
||||
SelectionChanged="TimeZoneBox_OnSelectionChanged"
|
||||
Text="{Binding Path=TimeZone, Mode=OneWay}"
|
||||
TextChanged="TimeZoneBox_OnTextChanged"
|
||||
ValueMemberBinding="{ReflectionBinding TzMultiBinding}"
|
||||
ToolTip.Tip="{locale:Locale TimezoneTooltip}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,0,10" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemSystemTime}"
|
||||
ToolTip.Tip="{locale:Locale TimeTooltip}"
|
||||
Width="250"/>
|
||||
<DatePicker VerticalAlignment="Center" SelectedDate="{Binding DateOffset}"
|
||||
ToolTip.Tip="{locale:Locale TimeTooltip}"
|
||||
Width="350" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="250,0,0,10" Orientation="Horizontal">
|
||||
<TimePicker
|
||||
VerticalAlignment="Center"
|
||||
ClockIdentifier="24HourClock"
|
||||
SelectedTime="{Binding TimeOffset}"
|
||||
Width="350"
|
||||
ToolTip.Tip="{locale:Locale TimeTooltip}" />
|
||||
</StackPanel>
|
||||
<CheckBox IsChecked="{Binding EnableVsync}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnableVsync}"
|
||||
ToolTip.Tip="{locale:Locale VSyncToggleTooltip}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableFsIntegrityChecks}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnableFsIntegrityChecks}"
|
||||
ToolTip.Tip="{locale:Locale FsIntegrityToggleTooltip}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabSystemHacks}" />
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemHacksNote}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding ExpandDramSize}"
|
||||
ToolTip.Tip="{locale:Locale DRamTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemExpandDramSize}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding IgnoreMissingServices}"
|
||||
ToolTip.Tip="{locale:Locale IgnoreMissingServicesTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemIgnoreMissingServices}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="CpuPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuCache}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnablePptc}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnablePptc}"
|
||||
ToolTip.Tip="{locale:Locale PptcToggleTooltip}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabCpuMemory}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemMemoryManagerMode}"
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding MemoryMode}"
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerTooltip}"
|
||||
HorizontalContentAlignment="Left"
|
||||
Width="350">
|
||||
<ComboBoxItem
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerSoftwareTooltip}">
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemMemoryManagerModeSoftware}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerHostTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemMemoryManagerModeHost}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem
|
||||
ToolTip.Tip="{locale:Locale MemoryManagerUnsafeTooltip}">
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabSystemMemoryManagerModeHostUnchecked}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="GraphicsPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsAPI}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsBackend}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}"
|
||||
SelectedIndex="{Binding GraphicsBackendIndex}">
|
||||
<ComboBoxItem IsVisible="{Binding IsVulkanAvailable}">
|
||||
<TextBlock Text="Vulkan" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
|
||||
<TextBlock Text="OpenGL" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsPreferredGpu}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}"
|
||||
SelectedIndex="{Binding PreferredGpuIndex}"
|
||||
Items="{Binding AvailableGpus}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsFeatures}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableShaderCache}"
|
||||
ToolTip.Tip="{locale:Locale ShaderCacheToggleTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsEnableShaderCache}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableTextureRecompression}"
|
||||
ToolTip.Tip="{locale:Locale SettingsEnableTextureRecompressionTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsEnableTextureRecompression}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableMacroHLE}"
|
||||
ToolTip.Tip="{locale:Locale SettingsEnableMacroHLETooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsEnableMacroHLE}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsResolutionScale}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding ResolutionScale}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale ResolutionScaleTooltip}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleCustom}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScaleNative}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale2x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale3x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsResolutionScale4x}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<ui:NumberBox
|
||||
Margin="10,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale ResolutionScaleEntryTooltip}"
|
||||
MinWidth="150"
|
||||
SmallChange="0.1"
|
||||
LargeChange="1"
|
||||
SimpleNumberFormat="F2"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
IsVisible="{Binding IsCustomResolutionScaleActive}"
|
||||
Maximum="100"
|
||||
Minimum="0.1"
|
||||
Value="{Binding CustomResolutionScale}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale AnisotropyTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding MaxAnisotropy}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale AnisotropyTooltip}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale AspectRatioTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsAspectRatio}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding AspectRatio}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale AspectRatioTooltip}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio4x3}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x9}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio16x10}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio21x9}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatio32x9}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabGraphicsAspectRatioStretch}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale GraphicsBackendThreadingTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsBackendMultithreading}"
|
||||
Width="250" />
|
||||
<ComboBox Width="350"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale GalThreadingTooltip}"
|
||||
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale CommonAuto}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale CommonOff}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale CommonOn}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabGraphicsDeveloperOptions}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}"
|
||||
Text="{locale:Locale SettingsTabGraphicsShaderDumpPath}"
|
||||
Width="250" />
|
||||
<TextBox Text="{Binding ShaderDumpPath}"
|
||||
Width="350"
|
||||
ToolTip.Tip="{locale:Locale ShaderDumpPathTooltip}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="AudioPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabAudio}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemAudioBackend}"
|
||||
ToolTip.Tip="{locale:Locale AudioBackendTooltip}"
|
||||
Width="250" />
|
||||
<ComboBox SelectedIndex="{Binding AudioBackend}"
|
||||
Width="350"
|
||||
HorizontalContentAlignment="Left">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendDummy}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsOpenAlEnabled}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendOpenAL}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsSoundIoEnabled}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSoundIO}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem IsEnabled="{Binding IsSDL2Enabled}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemAudioBackendSDL2}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabSystemAudioVolume}"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Width="250" />
|
||||
<ui:NumberBox Value="{Binding Volume}"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Width="350"
|
||||
SmallChange="1"
|
||||
LargeChange="10"
|
||||
SimpleNumberFormat="F0"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Minimum="0"
|
||||
Maximum="100" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
<Slider Value="{Binding Volume}"
|
||||
Margin="250,0,0,0"
|
||||
ToolTip.Tip="{locale:Locale AudioVolumeTooltip}"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
SmallChange="5"
|
||||
TickFrequency="5"
|
||||
IsSnapToTickEnabled="True"
|
||||
LargeChange="10"
|
||||
Width="350" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="NetworkPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabNetworkConnection}" />
|
||||
<CheckBox Margin="10,0,0,0" IsChecked="{Binding EnableInternetAccess}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabSystemEnableInternetAccess}"
|
||||
ToolTip.Tip="{locale:Locale EnableInternetAccessTooltip}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<ScrollViewer
|
||||
Name="LoggingPage"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Border Classes="settings">
|
||||
<StackPanel
|
||||
Margin="10,5"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingLogging}" />
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableFileLog}"
|
||||
ToolTip.Tip="{locale:Locale FileLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableLoggingToFile}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableStub}"
|
||||
ToolTip.Tip="{locale:Locale StubLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableStubLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableInfo}"
|
||||
ToolTip.Tip="{locale:Locale InfoLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableInfoLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableWarn}"
|
||||
ToolTip.Tip="{locale:Locale WarnLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableWarningLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableError}"
|
||||
ToolTip.Tip="{locale:Locale ErrorLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableErrorLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableTrace}"
|
||||
ToolTip.Tip="{locale:Locale TraceLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableTraceLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableGuest}"
|
||||
ToolTip.Tip="{locale:Locale GuestLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableGuestLogs}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
<Separator Height="1" />
|
||||
<TextBlock Classes="h1" Text="{locale:Locale SettingsTabLoggingDeveloperOptions}" />
|
||||
<StackPanel
|
||||
Margin="10,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding EnableDebug}"
|
||||
ToolTip.Tip="{locale:Locale DebugLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableDebugLogs}" />
|
||||
</CheckBox>
|
||||
<CheckBox IsChecked="{Binding EnableFsAccessLog}"
|
||||
ToolTip.Tip="{locale:Locale FileAccessLogTooltip}">
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingEnableFsAccessLogs}" />
|
||||
</CheckBox>
|
||||
<StackPanel Margin="0,10,0,0" Orientation="Horizontal" VerticalAlignment="Stretch">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
ToolTip.Tip="{locale:Locale FSAccessLogModeTooltip}"
|
||||
Text="{locale:Locale SettingsTabLoggingFsGlobalAccessLogMode}"
|
||||
Width="285" />
|
||||
<ui:NumberBox
|
||||
Maximum="3"
|
||||
Minimum="0"
|
||||
Width="150"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
SmallChange="1"
|
||||
LargeChange="1"
|
||||
Value="{Binding FsGlobalAccessLogMode}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,10,0,0" Orientation="Horizontal">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevel}"
|
||||
ToolTip.Tip="{locale:Locale OpenGlLogLevel}"
|
||||
Width="285" />
|
||||
<ComboBox SelectedIndex="{Binding OpenglDebugLevel}"
|
||||
Width="150"
|
||||
HorizontalContentAlignment="Left"
|
||||
ToolTip.Tip="{locale:Locale OpenGlLogLevel}">
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelNone}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelError}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock
|
||||
Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelPerformance}" />
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
<TextBlock Text="{locale:Locale SettingsTabLoggingGraphicsBackendLogLevelAll}" />
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
<settings:SettingsUIView Name="UiPage" />
|
||||
<settings:SettingsInputView Name="InputPage" />
|
||||
<settings:SettingsHotkeysView Name="HotkeysPage" />
|
||||
<settings:SettingsSystemView Name="SystemPage" />
|
||||
<settings:SettingsCPUView Name="CpuPage" />
|
||||
<settings:SettingsGraphicsView Name="GraphicsPage" />
|
||||
<settings:SettingsAudioView Name="AudioPage" />
|
||||
<settings:SettingsNetworkView Name="NetworkPage" />
|
||||
<settings:SettingsLoggingView Name="LoggingPage" />
|
||||
</Grid>
|
||||
<ui:NavigationView Grid.Row="1"
|
||||
IsSettingsVisible="False"
|
||||
@@ -962,7 +102,7 @@
|
||||
Spacing="10"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
ReverseOrder="{ReflectionBinding IsMacOS}">
|
||||
ReverseOrder="{Binding IsMacOS}">
|
||||
<Button
|
||||
HotKey="Enter"
|
||||
Classes="accent"
|
||||
|
@@ -1,49 +1,29 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using FluentAvalonia.Core;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.Assigner;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Windows
|
||||
{
|
||||
public partial class SettingsWindow : StyleableWindow
|
||||
{
|
||||
private ButtonKeyAssigner _currentAssigner;
|
||||
|
||||
internal SettingsViewModel ViewModel { get; set; }
|
||||
|
||||
public SettingsWindow(VirtualFileSystem virtualFileSystem, ContentManager contentManager)
|
||||
{
|
||||
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.Settings]}";
|
||||
|
||||
ViewModel = new SettingsViewModel(virtualFileSystem, contentManager, this);
|
||||
ViewModel = new SettingsViewModel(virtualFileSystem, contentManager);
|
||||
DataContext = ViewModel;
|
||||
|
||||
ViewModel.CloseWindow += Close;
|
||||
ViewModel.SaveSettingsEvent += SaveSettings;
|
||||
|
||||
InitializeComponent();
|
||||
Load();
|
||||
|
||||
FuncMultiValueConverter<string, string> converter = new(parts => string.Format("{0} {1} {2}", parts.ToArray()).Trim());
|
||||
MultiBinding tzMultiBinding = new() { Converter = converter };
|
||||
tzMultiBinding.Bindings.Add(new Binding("UtcDifference"));
|
||||
tzMultiBinding.Bindings.Add(new Binding("Location"));
|
||||
tzMultiBinding.Bindings.Add(new Binding("Abbreviation"));
|
||||
|
||||
TimeZoneBox.ValueMemberBinding = tzMultiBinding;
|
||||
}
|
||||
|
||||
public SettingsWindow()
|
||||
@@ -55,6 +35,16 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
Load();
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
InputPage.ControllerSettings?.SaveCurrentProfile();
|
||||
|
||||
if (Owner is MainWindow window && ViewModel.DirectoryChanged)
|
||||
{
|
||||
window.ViewModel.LoadApplications();
|
||||
}
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
Pages.Children.Clear();
|
||||
@@ -62,152 +52,52 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
NavPanel.SelectedItem = NavPanel.MenuItems.ElementAt(0);
|
||||
}
|
||||
|
||||
private void Button_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is ToggleButton button)
|
||||
{
|
||||
if (_currentAssigner != null && button == _currentAssigner.ToggledButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentAssigner == null && (bool)button.IsChecked)
|
||||
{
|
||||
_currentAssigner = new ButtonKeyAssigner(button);
|
||||
|
||||
FocusManager.Instance.Focus(this, NavigationMethod.Pointer);
|
||||
|
||||
PointerPressed += MouseClick;
|
||||
|
||||
IKeyboard keyboard = (IKeyboard)ViewModel.AvaloniaKeyboardDriver.GetGamepad(ViewModel.AvaloniaKeyboardDriver.GamepadsIds[0]);
|
||||
IButtonAssigner assigner = new KeyboardKeyAssigner(keyboard);
|
||||
|
||||
_currentAssigner.GetInputAndAssign(assigner);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_currentAssigner != null)
|
||||
{
|
||||
ToggleButton oldButton = _currentAssigner.ToggledButton;
|
||||
|
||||
_currentAssigner.Cancel();
|
||||
_currentAssigner = null;
|
||||
|
||||
button.IsChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Button_Unchecked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
}
|
||||
|
||||
private void MouseClick(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
bool shouldUnbind = false;
|
||||
|
||||
if (e.GetCurrentPoint(this).Properties.IsMiddleButtonPressed)
|
||||
{
|
||||
shouldUnbind = true;
|
||||
}
|
||||
|
||||
_currentAssigner?.Cancel(shouldUnbind);
|
||||
|
||||
PointerPressed -= MouseClick;
|
||||
}
|
||||
|
||||
private void NavPanelOnSelectionChanged(object sender, NavigationViewSelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.SelectedItem is NavigationViewItem navitem)
|
||||
if (e.SelectedItem is NavigationViewItem navItem && navItem.Tag is not null)
|
||||
{
|
||||
NavPanel.Content = navitem.Tag.ToString() switch
|
||||
switch (navItem.Tag.ToString())
|
||||
{
|
||||
"UiPage" => UiPage,
|
||||
"InputPage" => InputPage,
|
||||
"HotkeysPage" => HotkeysPage,
|
||||
"SystemPage" => SystemPage,
|
||||
"CpuPage" => CpuPage,
|
||||
"GraphicsPage" => GraphicsPage,
|
||||
"AudioPage" => AudioPage,
|
||||
"NetworkPage" => NetworkPage,
|
||||
"LoggingPage" => LoggingPage,
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
string path = PathBox.Text;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path))
|
||||
{
|
||||
ViewModel.GameDirectories.Add(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = await new OpenFolderDialog().ShowAsync(this);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
ViewModel.GameDirectories.Add(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
case "UiPage":
|
||||
UiPage.ViewModel = ViewModel;
|
||||
NavPanel.Content = UiPage;
|
||||
break;
|
||||
case "InputPage":
|
||||
NavPanel.Content = InputPage;
|
||||
break;
|
||||
case "HotkeysPage":
|
||||
NavPanel.Content = HotkeysPage;
|
||||
break;
|
||||
case "SystemPage":
|
||||
SystemPage.ViewModel = ViewModel;
|
||||
NavPanel.Content = SystemPage;
|
||||
break;
|
||||
case "CpuPage":
|
||||
NavPanel.Content = CpuPage;
|
||||
break;
|
||||
case "GraphicsPage":
|
||||
NavPanel.Content = GraphicsPage;
|
||||
break;
|
||||
case "AudioPage":
|
||||
NavPanel.Content = AudioPage;
|
||||
break;
|
||||
case "NetworkPage":
|
||||
NavPanel.Content = NetworkPage;
|
||||
break;
|
||||
case "LoggingPage":
|
||||
NavPanel.Content = LoggingPage;
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveButton_OnClick(object sender, RoutedEventArgs e)
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
int oldIndex = GameList.SelectedIndex;
|
||||
|
||||
foreach (string path in new List<string>(GameList.SelectedItems.Cast<string>()))
|
||||
{
|
||||
ViewModel.GameDirectories.Remove(path);
|
||||
ViewModel.DirectoryChanged = true;
|
||||
}
|
||||
|
||||
if (GameList.ItemCount > 0)
|
||||
{
|
||||
GameList.SelectedIndex = oldIndex < GameList.ItemCount ? oldIndex : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void TimeZoneBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.AddedItems != null && e.AddedItems.Count > 0)
|
||||
{
|
||||
if (e.AddedItems[0] is TimeZone timeZone)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TimeZoneBox_OnTextChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is AutoCompleteBox box)
|
||||
{
|
||||
if (box.SelectedItem != null && box.SelectedItem is TimeZone timeZone)
|
||||
{
|
||||
ViewModel.ValidateAndSetTimeZone(timeZone.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
ControllerSettings.Dispose();
|
||||
|
||||
_currentAssigner?.Cancel();
|
||||
_currentAssigner = null;
|
||||
|
||||
base.OnClosed(e);
|
||||
HotkeysPage.Dispose();
|
||||
InputPage.Dispose();
|
||||
base.OnClosing(e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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";
|
||||
|
||||
@@ -50,4 +50,4 @@ namespace Ryujinx.Common
|
||||
return AppDomain.CurrentDomain.BaseDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
||||
|
@@ -59,7 +59,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Version = ReleaseInformations.GetVersion();
|
||||
Version = ReleaseInformation.GetVersion();
|
||||
|
||||
Console.Title = $"Ryujinx Console {Version} (Headless SDL2)";
|
||||
|
||||
@@ -419,7 +419,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
if ((bool)option.EnableFileLog)
|
||||
{
|
||||
Logger.AddTarget(new AsyncLogTargetWrapper(
|
||||
new FileLogTarget(ReleaseInformations.GetBaseApplicationDirectory(), "file"),
|
||||
new FileLogTarget(ReleaseInformation.GetBaseApplicationDirectory(), "file"),
|
||||
1000,
|
||||
AsyncLogTargetOverflowAction.Block
|
||||
));
|
||||
|
@@ -100,6 +100,14 @@ 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);
|
||||
@@ -115,4 +123,4 @@ namespace Ryujinx.Horizon.Common
|
||||
return PrintableResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -123,44 +123,51 @@ namespace Ryujinx.Horizon.Generators.Hipc
|
||||
{
|
||||
string[] args = new string[method.ParameterList.Parameters.Count];
|
||||
|
||||
int index = 0;
|
||||
|
||||
foreach (var parameter in method.ParameterList.Parameters)
|
||||
if (args.Length == 0)
|
||||
{
|
||||
string canonicalTypeName = GetCanonicalTypeNameWithGenericArguments(compilation, parameter.Type);
|
||||
CommandArgType argType = GetCommandArgType(compilation, parameter);
|
||||
generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, Array.Empty<CommandArg>()) }},");
|
||||
}
|
||||
else
|
||||
{
|
||||
int index = 0;
|
||||
|
||||
string arg;
|
||||
|
||||
if (argType == CommandArgType.Buffer)
|
||||
foreach (var parameter in method.ParameterList.Parameters)
|
||||
{
|
||||
string bufferFlags = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 0);
|
||||
string bufferFixedSize = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 1);
|
||||
string canonicalTypeName = GetCanonicalTypeNameWithGenericArguments(compilation, parameter.Type);
|
||||
CommandArgType argType = GetCommandArgType(compilation, parameter);
|
||||
|
||||
if (bufferFixedSize != null)
|
||||
string arg;
|
||||
|
||||
if (argType == CommandArgType.Buffer)
|
||||
{
|
||||
arg = $"new CommandArg({bufferFlags}, {bufferFixedSize})";
|
||||
string bufferFlags = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 0);
|
||||
string bufferFixedSize = GetFirstAttributeAgument(compilation, parameter, TypeBufferAttribute, 1);
|
||||
|
||||
if (bufferFixedSize != null)
|
||||
{
|
||||
arg = $"new CommandArg({bufferFlags}, {bufferFixedSize})";
|
||||
}
|
||||
else
|
||||
{
|
||||
arg = $"new CommandArg({bufferFlags})";
|
||||
}
|
||||
}
|
||||
else if (argType == CommandArgType.InArgument || argType == CommandArgType.OutArgument)
|
||||
{
|
||||
string alignment = GetTypeAlignmentExpression(compilation, parameter.Type);
|
||||
|
||||
arg = $"new CommandArg(CommandArgType.{argType}, Unsafe.SizeOf<{canonicalTypeName}>(), {alignment})";
|
||||
}
|
||||
else
|
||||
{
|
||||
arg = $"new CommandArg({bufferFlags})";
|
||||
arg = $"new CommandArg(CommandArgType.{argType})";
|
||||
}
|
||||
}
|
||||
else if (argType == CommandArgType.InArgument || argType == CommandArgType.OutArgument)
|
||||
{
|
||||
string alignment = GetTypeAlignmentExpression(compilation, parameter.Type);
|
||||
|
||||
arg = $"new CommandArg(CommandArgType.{argType}, Unsafe.SizeOf<{canonicalTypeName}>(), {alignment})";
|
||||
}
|
||||
else
|
||||
{
|
||||
arg = $"new CommandArg(CommandArgType.{argType})";
|
||||
args[index++] = arg;
|
||||
}
|
||||
|
||||
args[index++] = arg;
|
||||
generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, {string.Join(", ", args)}) }},");
|
||||
}
|
||||
|
||||
generator.AppendLine($"{{ {commandId}, new CommandHandler({method.Identifier.Text}, {string.Join(", ", args)}) }},");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,12 +12,12 @@ namespace Ryujinx.Horizon
|
||||
private struct Range : IComparable<Range>
|
||||
{
|
||||
public ulong Offset { get; }
|
||||
public ulong Size { get; }
|
||||
public ulong Size { get; }
|
||||
|
||||
public Range(ulong offset, ulong size)
|
||||
{
|
||||
Offset = offset;
|
||||
Size = size;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public int CompareTo(Range other)
|
||||
@@ -31,7 +31,7 @@ namespace Ryujinx.Horizon
|
||||
|
||||
public HeapAllocator()
|
||||
{
|
||||
_freeRanges = new List<Range>();
|
||||
_freeRanges = new List<Range>();
|
||||
_currentHeapSize = 0;
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ namespace Ryujinx.Horizon
|
||||
var range = _freeRanges[i];
|
||||
|
||||
ulong alignedOffset = BitUtils.AlignUp(range.Offset, alignment);
|
||||
ulong sizeDelta = alignedOffset - range.Offset;
|
||||
ulong usableSize = range.Size - sizeDelta;
|
||||
ulong sizeDelta = alignedOffset - range.Offset;
|
||||
ulong usableSize = range.Size - sizeDelta;
|
||||
|
||||
if (sizeDelta < range.Size && usableSize >= size)
|
||||
{
|
||||
@@ -82,7 +82,7 @@ namespace Ryujinx.Horizon
|
||||
InsertFreeRange(range.Offset, sizeDelta);
|
||||
}
|
||||
|
||||
ulong endOffset = range.Offset + range.Size;
|
||||
ulong endOffset = range.Offset + range.Size;
|
||||
ulong remainingSize = endOffset - (alignedOffset + size);
|
||||
if (remainingSize != 0)
|
||||
{
|
||||
|
@@ -2,11 +2,13 @@ namespace Ryujinx.Horizon
|
||||
{
|
||||
public struct HorizonOptions
|
||||
{
|
||||
public bool IgnoreMissingServices { get; }
|
||||
public bool IgnoreMissingServices { get; }
|
||||
public bool ThrowOnInvalidCommandIds { get; }
|
||||
|
||||
public HorizonOptions(bool ignoreMissingServices)
|
||||
{
|
||||
IgnoreMissingServices = ignoreMissingServices;
|
||||
IgnoreMissingServices = ignoreMissingServices;
|
||||
ThrowOnInvalidCommandIds = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -21,24 +21,24 @@ namespace Ryujinx.Horizon
|
||||
[ThreadStatic]
|
||||
private static int _threadHandle;
|
||||
|
||||
public static HorizonOptions Options => _options;
|
||||
public static ISyscallApi Syscall => _syscall;
|
||||
public static IVirtualMemoryManager AddressSpace => _addressSpace;
|
||||
public static IThreadContext ThreadContext => _threadContext;
|
||||
public static int CurrentThreadHandle => _threadHandle;
|
||||
public static HorizonOptions Options => _options;
|
||||
public static ISyscallApi Syscall => _syscall;
|
||||
public static IVirtualMemoryManager AddressSpace => _addressSpace;
|
||||
public static IThreadContext ThreadContext => _threadContext;
|
||||
public static int CurrentThreadHandle => _threadHandle;
|
||||
|
||||
public static void Register(
|
||||
HorizonOptions options,
|
||||
ISyscallApi syscallApi,
|
||||
HorizonOptions options,
|
||||
ISyscallApi syscallApi,
|
||||
IVirtualMemoryManager addressSpace,
|
||||
IThreadContext threadContext,
|
||||
int threadHandle)
|
||||
IThreadContext threadContext,
|
||||
int threadHandle)
|
||||
{
|
||||
_options = options;
|
||||
_syscall = syscallApi;
|
||||
_addressSpace = addressSpace;
|
||||
_options = options;
|
||||
_syscall = syscallApi;
|
||||
_addressSpace = addressSpace;
|
||||
_threadContext = threadContext;
|
||||
_threadHandle = threadHandle;
|
||||
_threadHandle = threadHandle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
|
||||
LogPacketHeader header = reader.Read<LogPacketHeader>();
|
||||
|
||||
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,16 +108,16 @@ 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)
|
||||
{
|
||||
int result = 0;
|
||||
int count = 0;
|
||||
int count = 0;
|
||||
|
||||
byte encoded;
|
||||
|
||||
@@ -136,4 +133,4 @@ namespace Ryujinx.Horizon.LogManager
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,18 +2,19 @@
|
||||
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
|
||||
{
|
||||
@@ -9,36 +9,25 @@ namespace Ryujinx.Horizon.LogManager
|
||||
private const int LogMaxSessionsCount = 42;
|
||||
|
||||
private const int PointerBufferSize = 0x400;
|
||||
private const int MaxDomains = 31;
|
||||
private const int MaxDomainObjects = 61;
|
||||
private const int MaxDomains = 31;
|
||||
private const int MaxDomainObjects = 61;
|
||||
private const int MaxPortsCount = 1;
|
||||
|
||||
private const int MaxPortsCount = 1;
|
||||
private static readonly ManagerOptions _logManagerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
|
||||
|
||||
private static readonly ManagerOptions _logManagerOptions = new ManagerOptions(
|
||||
PointerBufferSize,
|
||||
MaxDomains,
|
||||
MaxDomainObjects,
|
||||
false);
|
||||
|
||||
private static readonly ServiceName _logServiceName = ServiceName.Encode("lm");
|
||||
|
||||
private SmApi _sm;
|
||||
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()
|
||||
@@ -51,4 +40,4 @@ namespace Ryujinx.Horizon.LogManager
|
||||
_serverManager.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,13 +2,16 @@
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
@@ -10,15 +10,15 @@ namespace Ryujinx.Horizon.Sdk
|
||||
public static Result SendRequest(out CmifResponse response, int sessionHandle, uint requestId, bool sendPid, scoped ReadOnlySpan<byte> data)
|
||||
{
|
||||
ulong tlsAddress = HorizonStatic.ThreadContext.TlsAddress;
|
||||
int tlsSize = Api.TlsMessageBufferSize;
|
||||
int tlsSize = Api.TlsMessageBufferSize;
|
||||
|
||||
using (var tlsRegion = HorizonStatic.AddressSpace.GetWritableRegion(tlsAddress, tlsSize))
|
||||
{
|
||||
CmifRequest request = CmifMessage.CreateRequest(tlsRegion.Memory.Span, new CmifRequestFormat()
|
||||
{
|
||||
DataSize = data.Length,
|
||||
DataSize = data.Length,
|
||||
RequestId = requestId,
|
||||
SendPid = sendPid
|
||||
SendPid = sendPid
|
||||
});
|
||||
|
||||
data.CopyTo(request.Data);
|
||||
@@ -29,10 +29,11 @@ namespace Ryujinx.Horizon.Sdk
|
||||
if (result.IsFailure)
|
||||
{
|
||||
response = default;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return CmifMessage.ParseResponse(out response, HorizonStatic.AddressSpace.GetWritableRegion(tlsAddress, tlsSize).Memory.Span, false, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,10 +3,10 @@
|
||||
struct CmifDomainInHeader
|
||||
{
|
||||
public CmifDomainRequestType Type;
|
||||
public byte ObjectsCount;
|
||||
public ushort DataSize;
|
||||
public int ObjectId;
|
||||
public uint Padding;
|
||||
public uint Token;
|
||||
public byte ObjectsCount;
|
||||
public ushort DataSize;
|
||||
public int ObjectId;
|
||||
public uint Padding;
|
||||
public uint Token;
|
||||
}
|
||||
}
|
||||
|
@@ -2,8 +2,8 @@
|
||||
{
|
||||
enum CmifDomainRequestType : byte
|
||||
{
|
||||
Invalid = 0,
|
||||
Invalid = 0,
|
||||
SendMessage = 1,
|
||||
Close = 2
|
||||
Close = 2
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
{
|
||||
static class CmifMessage
|
||||
{
|
||||
public const uint CmifInHeaderMagic = 0x49434653; // SFCI
|
||||
public const uint CmifInHeaderMagic = 0x49434653; // SFCI
|
||||
public const uint CmifOutHeaderMagic = 0x4f434653; // SFCO
|
||||
|
||||
public static CmifRequest CreateRequest(Span<byte> output, CmifRequestFormat format)
|
||||
@@ -21,27 +21,31 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
}
|
||||
|
||||
totalSize += Unsafe.SizeOf<CmifInHeader>() + format.DataSize;
|
||||
totalSize = (totalSize + 1) & ~1;
|
||||
totalSize = (totalSize + 1) & ~1;
|
||||
|
||||
int outPointerSizeTableOffset = totalSize;
|
||||
int outPointerSizeTableSize = format.OutAutoBuffersCount + format.OutPointersCount;
|
||||
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()
|
||||
{
|
||||
Type = format.Context != 0 ? (int)CommandType.RequestWithContext : (int)CommandType.Request,
|
||||
SendStaticsCount = format.InAutoBuffersCount + format.InPointersCount,
|
||||
SendBuffersCount = format.InAutoBuffersCount + format.InBuffersCount,
|
||||
ReceiveBuffersCount = format.OutAutoBuffersCount + format.OutBuffersCount,
|
||||
ExchangeBuffersCount = format.InOutBuffersCount,
|
||||
DataWordsCount = rawDataSizeInWords,
|
||||
ReceiveStaticsCount = outPointerSizeTableSize + format.OutFixedPointersCount,
|
||||
SendPid = format.SendPid,
|
||||
CopyHandlesCount = format.HandlesCount,
|
||||
MoveHandlesCount = 0
|
||||
});
|
||||
Hipc = HipcMessage.WriteMessage(output, new HipcMetadata()
|
||||
{
|
||||
Type = format.Context != 0 ? (int)CommandType.RequestWithContext : (int)CommandType.Request,
|
||||
SendStaticsCount = format.InAutoBuffersCount + format.InPointersCount,
|
||||
SendBuffersCount = format.InAutoBuffersCount + format.InBuffersCount,
|
||||
ReceiveBuffersCount = format.OutAutoBuffersCount + format.OutBuffersCount,
|
||||
ExchangeBuffersCount = format.InOutBuffersCount,
|
||||
DataWordsCount = rawDataSizeInWords,
|
||||
ReceiveStaticsCount = outPointerSizeTableSize + format.OutFixedPointersCount,
|
||||
SendPid = format.SendPid,
|
||||
CopyHandlesCount = format.HandlesCount,
|
||||
MoveHandlesCount = 0
|
||||
})
|
||||
};
|
||||
|
||||
Span<uint> data = request.Hipc.DataWords;
|
||||
|
||||
@@ -53,35 +57,36 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
|
||||
domainHeader = new CmifDomainInHeader()
|
||||
{
|
||||
Type = CmifDomainRequestType.SendMessage,
|
||||
Type = CmifDomainRequestType.SendMessage,
|
||||
ObjectsCount = (byte)format.ObjectsCount,
|
||||
DataSize = (ushort)payloadSize,
|
||||
ObjectId = format.ObjectId,
|
||||
Padding = 0,
|
||||
Token = format.Context
|
||||
DataSize = (ushort)payloadSize,
|
||||
ObjectId = format.ObjectId,
|
||||
Padding = 0,
|
||||
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];
|
||||
|
||||
header = new CmifInHeader()
|
||||
{
|
||||
Magic = CmifInHeaderMagic,
|
||||
Version = format.Context != 0 ? 1u : 0u,
|
||||
Magic = CmifInHeaderMagic,
|
||||
Version = format.Context != 0 ? 1u : 0u,
|
||||
CommandId = format.RequestId,
|
||||
Token = format.ObjectId != 0 ? 0u : format.Context
|
||||
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);
|
||||
request.OutPointerSizes = MemoryMarshal.Cast<byte, ushort>(outPointerTable);
|
||||
Span<byte> outPointerTable = MemoryMarshal.Cast<uint, byte>(request.Hipc.DataWords)[(outPointerSizeTableOffset - paddingSizeBefore)..];
|
||||
|
||||
request.OutPointerSizes = MemoryMarshal.Cast<byte, ushort>(outPointerTable);
|
||||
request.ServerPointerSize = format.ServerPointerSize;
|
||||
|
||||
return request;
|
||||
@@ -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<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,19 +110,21 @@ 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>()),
|
||||
Objects = objects,
|
||||
Data = data[Unsafe.SizeOf<CmifOutHeader>()..],
|
||||
Objects = objects,
|
||||
CopyHandles = responseMessage.Data.CopyHandles,
|
||||
MoveHandles = responseMessage.Data.MoveHandles
|
||||
};
|
||||
@@ -125,4 +132,4 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,10 +5,10 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
struct CmifOutHeader
|
||||
{
|
||||
#pragma warning disable CS0649
|
||||
public uint Magic;
|
||||
public uint Version;
|
||||
public uint Magic;
|
||||
public uint Version;
|
||||
public Result Result;
|
||||
public uint Token;
|
||||
public uint Token;
|
||||
#pragma warning restore CS0649
|
||||
}
|
||||
}
|
||||
|
@@ -6,9 +6,9 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
ref struct CmifRequest
|
||||
{
|
||||
public HipcMessageData Hipc;
|
||||
public Span<byte> Data;
|
||||
public Span<ushort> OutPointerSizes;
|
||||
public Span<uint> Objects;
|
||||
public int ServerPointerSize;
|
||||
public Span<byte> Data;
|
||||
public Span<ushort> OutPointerSizes;
|
||||
public Span<uint> Objects;
|
||||
public int ServerPointerSize;
|
||||
}
|
||||
}
|
||||
|
@@ -3,21 +3,21 @@
|
||||
struct CmifRequestFormat
|
||||
{
|
||||
#pragma warning disable CS0649
|
||||
public int ObjectId;
|
||||
public int ObjectId;
|
||||
public uint RequestId;
|
||||
public uint Context;
|
||||
public int DataSize;
|
||||
public int ServerPointerSize;
|
||||
public int InAutoBuffersCount;
|
||||
public int OutAutoBuffersCount;
|
||||
public int InBuffersCount;
|
||||
public int OutBuffersCount;
|
||||
public int InOutBuffersCount;
|
||||
public int InPointersCount;
|
||||
public int OutPointersCount;
|
||||
public int OutFixedPointersCount;
|
||||
public int ObjectsCount;
|
||||
public int HandlesCount;
|
||||
public int DataSize;
|
||||
public int ServerPointerSize;
|
||||
public int InAutoBuffersCount;
|
||||
public int OutAutoBuffersCount;
|
||||
public int InBuffersCount;
|
||||
public int OutBuffersCount;
|
||||
public int InOutBuffersCount;
|
||||
public int InPointersCount;
|
||||
public int OutPointersCount;
|
||||
public int OutFixedPointersCount;
|
||||
public int ObjectsCount;
|
||||
public int HandlesCount;
|
||||
public bool SendPid;
|
||||
#pragma warning restore CS0649
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
{
|
||||
public ReadOnlySpan<byte> Data;
|
||||
public ReadOnlySpan<uint> Objects;
|
||||
public ReadOnlySpan<int> CopyHandles;
|
||||
public ReadOnlySpan<int> MoveHandles;
|
||||
public ReadOnlySpan<int> CopyHandles;
|
||||
public ReadOnlySpan<int> MoveHandles;
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
{
|
||||
enum CommandType
|
||||
{
|
||||
Invalid = 0,
|
||||
LegacyRequest = 1,
|
||||
Close = 2,
|
||||
LegacyControl = 3,
|
||||
Request = 4,
|
||||
Control = 5,
|
||||
Invalid = 0,
|
||||
LegacyRequest = 1,
|
||||
Close = 2,
|
||||
LegacyControl = 3,
|
||||
Request = 4,
|
||||
Control = 5,
|
||||
RequestWithContext = 6,
|
||||
ControlWithContext = 7
|
||||
}
|
||||
|
@@ -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; }
|
||||
@@ -11,7 +11,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
public PointerAndSize(ulong address, ulong size)
|
||||
{
|
||||
Address = address;
|
||||
Size = size;
|
||||
Size = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,28 +2,29 @@
|
||||
{
|
||||
struct ServerMessageRuntimeMetadata
|
||||
{
|
||||
public ushort InDataSize { get; }
|
||||
public ushort OutDataSize { get; }
|
||||
public byte InHeadersSize { get; }
|
||||
public byte OutHeadersSize { get; }
|
||||
public byte InObjectsCount { get; }
|
||||
public byte OutObjectsCount { get; }
|
||||
public ushort InDataSize { get; }
|
||||
public ushort OutDataSize { get; }
|
||||
public byte InHeadersSize { get; }
|
||||
public byte OutHeadersSize { get; }
|
||||
public byte InObjectsCount { get; }
|
||||
public byte OutObjectsCount { get; }
|
||||
|
||||
public int UnfixedOutPointerSizeOffset => InDataSize + InHeadersSize + 0x10;
|
||||
|
||||
public ServerMessageRuntimeMetadata(
|
||||
ushort inDataSize,
|
||||
ushort outDataSize,
|
||||
byte inHeadersSize,
|
||||
byte outHeadersSize,
|
||||
byte inObjectsCount,
|
||||
byte outObjectsCount)
|
||||
byte inHeadersSize,
|
||||
byte outHeadersSize,
|
||||
byte inObjectsCount,
|
||||
byte outObjectsCount)
|
||||
{
|
||||
InDataSize = inDataSize;
|
||||
OutDataSize = outDataSize;
|
||||
InHeadersSize = inHeadersSize;
|
||||
OutHeadersSize = outHeadersSize;
|
||||
InObjectsCount = inObjectsCount;
|
||||
InDataSize = inDataSize;
|
||||
OutDataSize = outDataSize;
|
||||
InHeadersSize = inHeadersSize;
|
||||
OutHeadersSize = outHeadersSize;
|
||||
InObjectsCount = inObjectsCount;
|
||||
OutObjectsCount = outObjectsCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,14 +5,14 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
{
|
||||
ref struct ServiceDispatchContext
|
||||
{
|
||||
public IServiceObject ServiceObject;
|
||||
public ServerSessionManager Manager;
|
||||
public ServerSession Session;
|
||||
public IServiceObject ServiceObject;
|
||||
public ServerSessionManager Manager;
|
||||
public ServerSession Session;
|
||||
public ServerMessageProcessor Processor;
|
||||
public HandlesToClose HandlesToClose;
|
||||
public PointerAndSize PointerBuffer;
|
||||
public ReadOnlySpan<byte> InMessageBuffer;
|
||||
public Span<byte> OutMessageBuffer;
|
||||
public HipcMessage Request;
|
||||
public HandlesToClose HandlesToClose;
|
||||
public PointerAndSize PointerBuffer;
|
||||
public ReadOnlySpan<byte> InMessageBuffer;
|
||||
public Span<byte> OutMessageBuffer;
|
||||
public HipcMessage Request;
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,7 +12,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
public ServiceDispatchTable(string objectName, IReadOnlyDictionary<int, CommandHandler> entries)
|
||||
{
|
||||
_objectName = objectName;
|
||||
_entries = entries;
|
||||
_entries = entries;
|
||||
}
|
||||
|
||||
public override Result ProcessMessage(ref ServiceDispatchContext context, ReadOnlySpan<byte> inRawData)
|
||||
@@ -30,4 +30,4 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
|
||||
return new ServiceDispatchTable(instance.GetType().Name, instance.GetCommandHandlers());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,37 +20,37 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
|
||||
struct CommandArg
|
||||
{
|
||||
public CommandArgType Type { get; }
|
||||
public HipcBufferFlags BufferFlags { get; }
|
||||
public ushort BufferFixedSize { get; }
|
||||
public int ArgSize { get; }
|
||||
public int ArgAlignment { get; }
|
||||
public CommandArgType Type { get; }
|
||||
public HipcBufferFlags BufferFlags { get; }
|
||||
public ushort BufferFixedSize { get; }
|
||||
public int ArgSize { get; }
|
||||
public int ArgAlignment { get; }
|
||||
|
||||
public CommandArg(CommandArgType type)
|
||||
{
|
||||
Type = type;
|
||||
BufferFlags = default;
|
||||
Type = type;
|
||||
BufferFlags = default;
|
||||
BufferFixedSize = 0;
|
||||
ArgSize = 0;
|
||||
ArgAlignment = 0;
|
||||
ArgSize = 0;
|
||||
ArgAlignment = 0;
|
||||
}
|
||||
|
||||
public CommandArg(CommandArgType type, int argSize, int argAlignment)
|
||||
{
|
||||
Type = type;
|
||||
BufferFlags = default;
|
||||
Type = type;
|
||||
BufferFlags = default;
|
||||
BufferFixedSize = 0;
|
||||
ArgSize = argSize;
|
||||
ArgAlignment = argAlignment;
|
||||
ArgSize = argSize;
|
||||
ArgAlignment = argAlignment;
|
||||
}
|
||||
|
||||
public CommandArg(HipcBufferFlags flags, ushort fixedSize = 0)
|
||||
{
|
||||
Type = CommandArgType.Buffer;
|
||||
BufferFlags = flags;
|
||||
Type = CommandArgType.Buffer;
|
||||
BufferFlags = flags;
|
||||
BufferFixedSize = fixedSize;
|
||||
ArgSize = 0;
|
||||
ArgAlignment = 0;
|
||||
ArgSize = 0;
|
||||
ArgAlignment = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,8 +6,8 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
[AttributeUsage(AttributeTargets.Parameter)]
|
||||
class BufferAttribute : Attribute
|
||||
{
|
||||
public HipcBufferFlags Flags { get; }
|
||||
public ushort FixedSize { get; }
|
||||
public HipcBufferFlags Flags { get; }
|
||||
public ushort FixedSize { get; }
|
||||
|
||||
public BufferAttribute(HipcBufferFlags flags)
|
||||
{
|
||||
@@ -16,7 +16,7 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
|
||||
public BufferAttribute(HipcBufferFlags flags, ushort fixedSize)
|
||||
{
|
||||
Flags = flags | HipcBufferFlags.FixedSize;
|
||||
Flags = flags | HipcBufferFlags.FixedSize;
|
||||
FixedSize = fixedSize;
|
||||
}
|
||||
}
|
||||
@@ -35,4 +35,4 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
class MoveHandleAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,20 +9,20 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
class CommandHandler
|
||||
{
|
||||
public delegate Result MethodInvoke(
|
||||
ref ServiceDispatchContext context,
|
||||
HipcCommandProcessor processor,
|
||||
ref ServiceDispatchContext context,
|
||||
HipcCommandProcessor processor,
|
||||
ServerMessageRuntimeMetadata runtimeMetadata,
|
||||
ReadOnlySpan<byte> inRawData,
|
||||
ref Span<CmifOutHeader> outHeader);
|
||||
ReadOnlySpan<byte> inRawData,
|
||||
ref Span<CmifOutHeader> outHeader);
|
||||
|
||||
private readonly MethodInvoke _invoke;
|
||||
private readonly MethodInvoke _invoke;
|
||||
private readonly HipcCommandProcessor _processor;
|
||||
|
||||
public string MethodName => _invoke.Method.Name;
|
||||
|
||||
public CommandHandler(MethodInvoke invoke, params CommandArg[] args)
|
||||
{
|
||||
_invoke = invoke;
|
||||
_invoke = invoke;
|
||||
_processor = new HipcCommandProcessor(args);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
context.Processor.SetImplementationProcessor(_processor);
|
||||
}
|
||||
|
||||
var runtimeMetadata = context.Processor.GetRuntimeMetadata();
|
||||
Result result = context.Processor.PrepareForProcess(ref context, runtimeMetadata);
|
||||
var runtimeMetadata = context.Processor.GetRuntimeMetadata();
|
||||
Result result = context.Processor.PrepareForProcess(ref context, runtimeMetadata);
|
||||
|
||||
if (result.IsFailure)
|
||||
{
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -65,4 +66,4 @@ namespace Ryujinx.Horizon.Sdk.Sf
|
||||
response.MoveHandles[index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -51,7 +51,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
Result result = ReplyImpl(sessionHandle, messageBuffer);
|
||||
|
||||
result.AbortUnless(KernelResult.TimedOut, KernelResult.PortRemoteClosed);
|
||||
result.AbortOnFailureUnless(KernelResult.TimedOut, KernelResult.PortRemoteClosed);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
@@ -86,4 +86,4 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,12 +5,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
[Flags]
|
||||
enum HipcBufferFlags : byte
|
||||
{
|
||||
In = 1 << 0,
|
||||
Out = 1 << 1,
|
||||
MapAlias = 1 << 2,
|
||||
Pointer = 1 << 3,
|
||||
FixedSize = 1 << 4,
|
||||
AutoSelect = 1 << 5,
|
||||
In = 1 << 0,
|
||||
Out = 1 << 1,
|
||||
MapAlias = 1 << 2,
|
||||
Pointer = 1 << 3,
|
||||
FixedSize = 1 << 4,
|
||||
AutoSelect = 1 << 5,
|
||||
MapTransferAllowsNonSecure = 1 << 6,
|
||||
MapTransferAllowsNonDevice = 1 << 7
|
||||
}
|
||||
|
@@ -2,9 +2,9 @@
|
||||
{
|
||||
enum HipcBufferMode
|
||||
{
|
||||
Normal = 0,
|
||||
Normal = 0,
|
||||
NonSecure = 1,
|
||||
Invalid = 2,
|
||||
Invalid = 2,
|
||||
NonDevice = 3
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
@@ -112,4 +112,4 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -10,9 +10,9 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
public const int AutoReceiveStatic = byte.MaxValue;
|
||||
|
||||
public HipcMetadata Meta;
|
||||
public HipcMetadata Meta;
|
||||
public HipcMessageData Data;
|
||||
public ulong Pid;
|
||||
public ulong Pid;
|
||||
|
||||
public HipcMessage(Span<byte> data)
|
||||
{
|
||||
@@ -20,10 +20,10 @@ 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;
|
||||
int receiveStaticsCount = 0;
|
||||
ulong pid = 0;
|
||||
|
||||
if (header.ReceiveStaticMode != 0)
|
||||
{
|
||||
@@ -42,46 +42,44 @@ 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));
|
||||
pid = MemoryMarshal.Cast<byte, ulong>(data)[0];
|
||||
data = data[sizeof(ulong)..];
|
||||
}
|
||||
}
|
||||
|
||||
Meta = new HipcMetadata()
|
||||
{
|
||||
Type = (int)header.Type,
|
||||
SendStaticsCount = header.SendStaticsCount,
|
||||
SendBuffersCount = header.SendBuffersCount,
|
||||
ReceiveBuffersCount = header.ReceiveBuffersCount,
|
||||
Type = (int)header.Type,
|
||||
SendStaticsCount = header.SendStaticsCount,
|
||||
SendBuffersCount = header.SendBuffersCount,
|
||||
ReceiveBuffersCount = header.ReceiveBuffersCount,
|
||||
ExchangeBuffersCount = header.ExchangeBuffersCount,
|
||||
DataWordsCount = header.DataWordsCount,
|
||||
ReceiveStaticsCount = receiveStaticsCount,
|
||||
SendPid = specialHeader.SendPid,
|
||||
CopyHandlesCount = specialHeader.CopyHandlesCount,
|
||||
MoveHandlesCount = specialHeader.MoveHandlesCount
|
||||
DataWordsCount = header.DataWordsCount,
|
||||
ReceiveStaticsCount = receiveStaticsCount,
|
||||
SendPid = specialHeader.SendPid,
|
||||
CopyHandlesCount = specialHeader.CopyHandlesCount,
|
||||
MoveHandlesCount = specialHeader.MoveHandlesCount
|
||||
};
|
||||
|
||||
Data = CreateMessageData(Meta, data, initialLength);
|
||||
Pid = pid;
|
||||
Pid = pid;
|
||||
}
|
||||
|
||||
public static HipcMessageData WriteResponse(
|
||||
Span<byte> destination,
|
||||
int sendStaticCount,
|
||||
int dataWordsCount,
|
||||
int copyHandlesCount,
|
||||
int moveHandlesCount)
|
||||
int sendStaticCount,
|
||||
int dataWordsCount,
|
||||
int copyHandlesCount,
|
||||
int moveHandlesCount)
|
||||
{
|
||||
return WriteMessage(destination, new HipcMetadata()
|
||||
{
|
||||
SendStaticsCount = sendStaticCount,
|
||||
DataWordsCount = dataWordsCount,
|
||||
DataWordsCount = dataWordsCount,
|
||||
CopyHandlesCount = copyHandlesCount,
|
||||
MoveHandlesCount = moveHandlesCount
|
||||
});
|
||||
@@ -89,38 +87,37 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
|
||||
public static HipcMessageData WriteMessage(Span<byte> destination, HipcMetadata meta)
|
||||
{
|
||||
int initialLength = destination.Length;
|
||||
|
||||
int initialLength = destination.Length;
|
||||
bool hasSpecialHeader = meta.SendPid || meta.CopyHandlesCount != 0 || meta.MoveHandlesCount != 0;
|
||||
|
||||
MemoryMarshal.Cast<byte, Header>(destination)[0] = new Header()
|
||||
{
|
||||
Type = (CommandType)meta.Type,
|
||||
SendStaticsCount = meta.SendStaticsCount,
|
||||
SendBuffersCount = meta.SendBuffersCount,
|
||||
ReceiveBuffersCount = meta.ReceiveBuffersCount,
|
||||
Type = (CommandType)meta.Type,
|
||||
SendStaticsCount = meta.SendStaticsCount,
|
||||
SendBuffersCount = meta.SendBuffersCount,
|
||||
ReceiveBuffersCount = meta.ReceiveBuffersCount,
|
||||
ExchangeBuffersCount = meta.ExchangeBuffersCount,
|
||||
DataWordsCount = meta.DataWordsCount,
|
||||
ReceiveStaticMode = meta.ReceiveStaticsCount != 0 ? (meta.ReceiveStaticsCount != AutoReceiveStatic ? meta.ReceiveStaticsCount + 2 : 2) : 0,
|
||||
HasSpecialHeader = hasSpecialHeader
|
||||
DataWordsCount = meta.DataWordsCount,
|
||||
ReceiveStaticMode = meta.ReceiveStaticsCount != 0 ? (meta.ReceiveStaticsCount != AutoReceiveStatic ? meta.ReceiveStaticsCount + 2 : 2) : 0,
|
||||
HasSpecialHeader = hasSpecialHeader
|
||||
};
|
||||
|
||||
destination = destination.Slice(Unsafe.SizeOf<Header>());
|
||||
destination = destination[Unsafe.SizeOf<Header>()..];
|
||||
|
||||
if (hasSpecialHeader)
|
||||
{
|
||||
MemoryMarshal.Cast<byte, SpecialHeader>(destination)[0] = new SpecialHeader()
|
||||
{
|
||||
SendPid = meta.SendPid,
|
||||
SendPid = meta.SendPid,
|
||||
CopyHandlesCount = meta.CopyHandlesCount,
|
||||
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,68 +130,67 @@ 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;
|
||||
|
||||
if (meta.DataWordsCount != 0)
|
||||
{
|
||||
int dataOffset = initialLength - data.Length;
|
||||
int dataOffset = initialLength - data.Length;
|
||||
int dataOffsetAligned = BitUtils.AlignUp(dataOffset, 0x10);
|
||||
int padding = (dataOffsetAligned - dataOffset) / sizeof(uint);
|
||||
|
||||
int padding = (dataOffsetAligned - dataOffset) / sizeof(uint);
|
||||
dataWords = MemoryMarshal.Cast<byte, uint>(data)[padding..meta.DataWordsCount];
|
||||
|
||||
dataWords = MemoryMarshal.Cast<byte, uint>(data).Slice(padding, meta.DataWordsCount - padding);
|
||||
|
||||
data = data.Slice(meta.DataWordsCount * sizeof(uint));
|
||||
data = data[(meta.DataWordsCount * sizeof(uint))..];
|
||||
}
|
||||
|
||||
Span<HipcReceiveListEntry> receiveList = Span<HipcReceiveListEntry>.Empty;
|
||||
@@ -203,19 +199,19 @@ 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()
|
||||
{
|
||||
SendStatics = sendStatics,
|
||||
SendBuffers = sendBuffers,
|
||||
ReceiveBuffers = receiveBuffers,
|
||||
SendStatics = sendStatics,
|
||||
SendBuffers = sendBuffers,
|
||||
ReceiveBuffers = receiveBuffers,
|
||||
ExchangeBuffers = exchangeBuffers,
|
||||
DataWords = dataWords,
|
||||
ReceiveList = receiveList,
|
||||
CopyHandles = copyHandles,
|
||||
MoveHandles = moveHandles
|
||||
DataWords = dataWords,
|
||||
ReceiveList = receiveList,
|
||||
CopyHandles = copyHandles,
|
||||
MoveHandles = moveHandles
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -8,9 +8,9 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
public Span<HipcBufferDescriptor> SendBuffers;
|
||||
public Span<HipcBufferDescriptor> ReceiveBuffers;
|
||||
public Span<HipcBufferDescriptor> ExchangeBuffers;
|
||||
public Span<uint> DataWords;
|
||||
public Span<uint> DataWords;
|
||||
public Span<HipcReceiveListEntry> ReceiveList;
|
||||
public Span<int> CopyHandles;
|
||||
public Span<int> MoveHandles;
|
||||
public Span<int> CopyHandles;
|
||||
public Span<int> MoveHandles;
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,15 +2,15 @@
|
||||
{
|
||||
struct HipcMetadata
|
||||
{
|
||||
public int Type;
|
||||
public int SendStaticsCount;
|
||||
public int SendBuffersCount;
|
||||
public int ReceiveBuffersCount;
|
||||
public int ExchangeBuffersCount;
|
||||
public int DataWordsCount;
|
||||
public int ReceiveStaticsCount;
|
||||
public int Type;
|
||||
public int SendStaticsCount;
|
||||
public int SendBuffersCount;
|
||||
public int ReceiveBuffersCount;
|
||||
public int ExchangeBuffersCount;
|
||||
public int DataWordsCount;
|
||||
public int ReceiveStaticsCount;
|
||||
public bool SendPid;
|
||||
public int CopyHandlesCount;
|
||||
public int MoveHandlesCount;
|
||||
public int CopyHandlesCount;
|
||||
public int MoveHandlesCount;
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
public HipcReceiveListEntry(ulong address, ulong size)
|
||||
{
|
||||
_addressLow = (uint)address;
|
||||
_word1 = (ushort)(address >> 32) | (uint)(size << 16);
|
||||
_word1 = (ushort)(address >> 32) | (uint)(size << 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -4,9 +4,9 @@
|
||||
{
|
||||
private readonly ulong _data;
|
||||
|
||||
public ulong Address => ((((_data >> 2) & 0x70) | ((_data >> 12) & 0xf)) << 32) | (_data >> 32);
|
||||
public ushort Size => (ushort)(_data >> 16);
|
||||
public int ReceiveIndex => (int)(_data & 0xf);
|
||||
public ulong Address => ((((_data >> 2) & 0x70) | ((_data >> 12) & 0xf)) << 32) | (_data >> 32);
|
||||
public ushort Size => (ushort)(_data >> 16);
|
||||
public int ReceiveIndex => (int)(_data & 0xf);
|
||||
|
||||
public HipcStaticDescriptor(ulong address, ushort size, int receiveIndex)
|
||||
{
|
||||
@@ -19,4 +19,4 @@
|
||||
_data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,19 +2,19 @@
|
||||
{
|
||||
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; }
|
||||
public int MaxDomainObjects { get; }
|
||||
public int PointerBufferSize { get; }
|
||||
public int MaxDomains { get; }
|
||||
public int MaxDomainObjects { get; }
|
||||
public bool CanDeferInvokeRequest { get; }
|
||||
|
||||
public ManagerOptions(int pointerBufferSize, int maxDomains, int maxDomainObjects, bool canDeferInvokeRequest)
|
||||
{
|
||||
PointerBufferSize = pointerBufferSize;
|
||||
MaxDomains = maxDomains;
|
||||
MaxDomainObjects = maxDomainObjects;
|
||||
PointerBufferSize = pointerBufferSize;
|
||||
MaxDomains = maxDomains;
|
||||
MaxDomainObjects = maxDomainObjects;
|
||||
CanDeferInvokeRequest = canDeferInvokeRequest;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,22 +6,22 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
{
|
||||
class Server : MultiWaitHolderOfHandle
|
||||
{
|
||||
public int PortIndex { get; }
|
||||
public int PortHandle { get; }
|
||||
public ServiceName Name { get; }
|
||||
public bool Managed { get; }
|
||||
public int PortIndex { get; }
|
||||
public int PortHandle { get; }
|
||||
public ServiceName Name { get; }
|
||||
public bool Managed { get; }
|
||||
public ServiceObjectHolder StaticObject { get; }
|
||||
|
||||
public Server(
|
||||
int portIndex,
|
||||
int portHandle,
|
||||
ServiceName name,
|
||||
bool managed,
|
||||
int portIndex,
|
||||
int portHandle,
|
||||
ServiceName name,
|
||||
bool managed,
|
||||
ServiceObjectHolder staticHoder) : base(portHandle)
|
||||
{
|
||||
PortHandle = portHandle;
|
||||
Name = name;
|
||||
Managed = managed;
|
||||
Name = name;
|
||||
Managed = managed;
|
||||
|
||||
if (staticHoder != null)
|
||||
{
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -26,7 +26,7 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
|
||||
private enum UserDataTag
|
||||
{
|
||||
Server = 1,
|
||||
Server = 1,
|
||||
Session = 2
|
||||
}
|
||||
|
||||
@@ -36,16 +36,17 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
_canDeferInvokeRequest = options.CanDeferInvokeRequest;
|
||||
|
||||
_multiWait = new MultiWait();
|
||||
_waitList = new MultiWait();
|
||||
_waitList = new MultiWait();
|
||||
|
||||
_multiWaitSelectionLock = new object();
|
||||
_waitListLock = new object();
|
||||
_waitListLock = new object();
|
||||
|
||||
_requestStopEvent = new Event(EventClearMode.ManualClear);
|
||||
_notifyEvent = new Event(EventClearMode.ManualClear);
|
||||
_notifyEvent = new Event(EventClearMode.ManualClear);
|
||||
|
||||
_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());
|
||||
@@ -175,7 +183,8 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
|
||||
protected override void RegisterSessionToWaitList(ServerSession session)
|
||||
{
|
||||
session.HasReceived = false;
|
||||
session.UserData = UserDataTag.Session;
|
||||
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
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user