Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5c3cfb84c0 | ||
|
55557525b1 | ||
|
7e6342e44d | ||
|
c3555cb5d6 | ||
|
815819767c | ||
|
623604c391 | ||
|
617c5700ca | ||
|
7b62f7475e | ||
|
841dd56f4c | ||
|
a16d582a10 | ||
|
9ef0be477b | ||
|
c14ce4d2a5 | ||
|
171b46ef49 | ||
|
56fe2ff535 | ||
|
b1f8f868f6 | ||
|
d773d5152e | ||
|
33ba170315 | ||
|
638be5f296 | ||
|
49b37550ca | ||
|
a42f0bbb87 |
@@ -18,12 +18,13 @@
|
|||||||
<PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
|
<PackageVersion Include="GtkSharp.Dependencies" Version="1.1.1" />
|
||||||
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
|
<PackageVersion Include="GtkSharp.Dependencies.osx" Version="0.0.5" />
|
||||||
<PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" />
|
<PackageVersion Include="jp2masa.Avalonia.Flexbox" Version="0.3.0-beta.4" />
|
||||||
<PackageVersion Include="LibHac" Version="0.18.0" />
|
<PackageVersion Include="LibHac" Version="0.19.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||||
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
||||||
|
<PackageVersion Include="NetCoreServer" Version="7.0.0" />
|
||||||
<PackageVersion Include="NUnit" Version="3.13.3" />
|
<PackageVersion Include="NUnit" Version="3.13.3" />
|
||||||
<PackageVersion Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageVersion Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
<PackageVersion Include="OpenTK.Core" Version="4.7.7" />
|
<PackageVersion Include="OpenTK.Core" Version="4.7.7" />
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||||
<PackageVersion Include="Ryujinx.GtkSharp" Version="3.24.24.59-ryujinx" />
|
<PackageVersion Include="Ryujinx.GtkSharp" Version="3.24.24.59-ryujinx" />
|
||||||
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.28.1-build28" />
|
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.28.1-build28" />
|
||||||
|
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
||||||
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
||||||
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.16.0" />
|
<PackageVersion Include="Silk.NET.Vulkan" Version="2.16.0" />
|
||||||
|
@@ -141,3 +141,5 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY
|
|||||||
|
|
||||||
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
|
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
|
||||||
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
|
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
|
||||||
|
- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
|
||||||
|
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.
|
||||||
|
@@ -681,4 +681,33 @@
|
|||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
# ShellLink (MIT)
|
||||||
|
<details>
|
||||||
|
<summary>See License</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Yorick Koster, Securify B.V.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
@@ -3,8 +3,8 @@ Version=1.0
|
|||||||
Name=Ryujinx
|
Name=Ryujinx
|
||||||
Type=Application
|
Type=Application
|
||||||
Icon=Ryujinx
|
Icon=Ryujinx
|
||||||
Exec=env DOTNET_EnableAlternateStackCheck=1 Ryujinx %f
|
Exec=Ryujinx.sh %f
|
||||||
Comment=A Nintendo Switch Emulator
|
Comment=Plays Nintendo Switch applications
|
||||||
GenericName=Nintendo Switch Emulator
|
GenericName=Nintendo Switch Emulator
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Game;Emulator;
|
Categories=Game;Emulator;
|
||||||
|
13
distribution/linux/shortcut-template.desktop
Normal file
13
distribution/linux/shortcut-template.desktop
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Name={0}
|
||||||
|
Type=Application
|
||||||
|
Icon={1}
|
||||||
|
Exec={2} %f
|
||||||
|
Comment=Nintendo Switch application
|
||||||
|
GenericName=Nintendo Switch Emulator
|
||||||
|
Terminal=false
|
||||||
|
Categories=Game;Emulator;
|
||||||
|
Keywords=Switch;Nintendo;Emulator;
|
||||||
|
StartupWMClass=Ryujinx
|
||||||
|
PrefersNonDefaultGPU=true
|
35
distribution/macos/shortcut-template.plist
Normal file
35
distribution/macos/shortcut-template.plist
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>English</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{0}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{1}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>{2}</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<key>CSResourcesFileMapped</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>Copyright © 2018 - 2023 Ryujinx Team and Contributors.</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.games</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>11.0</string>
|
||||||
|
<key>UIPrerenderedIcon</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSEnvironment</key>
|
||||||
|
<dict>
|
||||||
|
<key>DOTNET_DefaultStackSize</key>
|
||||||
|
<string>200000</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@@ -38,7 +38,9 @@ namespace ARMeilleure.Decoders
|
|||||||
{
|
{
|
||||||
block = new Block(blkAddress);
|
block = new Block(blkAddress);
|
||||||
|
|
||||||
if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) || opsCount > instructionLimit || !memory.IsMapped(blkAddress))
|
if ((dMode != DecoderMode.MultipleBlocks && visited.Count >= 1) ||
|
||||||
|
opsCount > instructionLimit ||
|
||||||
|
(visited.Count > 0 && !memory.IsMapped(blkAddress)))
|
||||||
{
|
{
|
||||||
block.Exit = true;
|
block.Exit = true;
|
||||||
block.EndAddress = blkAddress;
|
block.EndAddress = blkAddress;
|
||||||
|
@@ -54,8 +54,6 @@ using System.Threading.Tasks;
|
|||||||
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
|
||||||
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
|
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
using InputManager = Ryujinx.Input.HLE.InputManager;
|
|
||||||
using IRenderer = Ryujinx.Graphics.GAL.IRenderer;
|
|
||||||
using Key = Ryujinx.Input.Key;
|
using Key = Ryujinx.Input.Key;
|
||||||
using MouseButton = Ryujinx.Input.MouseButton;
|
using MouseButton = Ryujinx.Input.MouseButton;
|
||||||
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
|
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
|
||||||
@@ -123,12 +121,14 @@ namespace Ryujinx.Ava
|
|||||||
public int Width { get; private set; }
|
public int Width { get; private set; }
|
||||||
public int Height { get; private set; }
|
public int Height { get; private set; }
|
||||||
public string ApplicationPath { get; private set; }
|
public string ApplicationPath { get; private set; }
|
||||||
|
public ulong ApplicationId { get; private set; }
|
||||||
public bool ScreenshotRequested { get; set; }
|
public bool ScreenshotRequested { get; set; }
|
||||||
|
|
||||||
public AppHost(
|
public AppHost(
|
||||||
RendererHost renderer,
|
RendererHost renderer,
|
||||||
InputManager inputManager,
|
InputManager inputManager,
|
||||||
string applicationPath,
|
string applicationPath,
|
||||||
|
ulong applicationId,
|
||||||
VirtualFileSystem virtualFileSystem,
|
VirtualFileSystem virtualFileSystem,
|
||||||
ContentManager contentManager,
|
ContentManager contentManager,
|
||||||
AccountManager accountManager,
|
AccountManager accountManager,
|
||||||
@@ -152,6 +152,7 @@ namespace Ryujinx.Ava
|
|||||||
NpadManager = _inputManager.CreateNpadManager();
|
NpadManager = _inputManager.CreateNpadManager();
|
||||||
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
TouchScreenManager = _inputManager.CreateTouchScreenManager();
|
||||||
ApplicationPath = applicationPath;
|
ApplicationPath = applicationPath;
|
||||||
|
ApplicationId = applicationId;
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
ContentManager = contentManager;
|
ContentManager = contentManager;
|
||||||
|
|
||||||
@@ -190,6 +191,7 @@ namespace Ryujinx.Ava
|
|||||||
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
|
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
|
||||||
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
|
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
|
||||||
|
|
||||||
|
ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
|
||||||
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
|
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
|
||||||
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
|
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
|
||||||
|
|
||||||
@@ -408,6 +410,11 @@ namespace Ryujinx.Ava
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs<bool> e)
|
||||||
|
{
|
||||||
|
Device.Configuration.EnableInternetAccess = e.NewValue;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs<string> e)
|
private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs<string> e)
|
||||||
{
|
{
|
||||||
Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
|
Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
|
||||||
@@ -635,7 +642,7 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
|
||||||
|
|
||||||
if (!Device.LoadXci(ApplicationPath))
|
if (!Device.LoadXci(ApplicationPath, ApplicationId))
|
||||||
{
|
{
|
||||||
Device.Dispose();
|
Device.Dispose();
|
||||||
|
|
||||||
@@ -662,7 +669,7 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
|
||||||
|
|
||||||
if (!Device.LoadNsp(ApplicationPath))
|
if (!Device.LoadNsp(ApplicationPath, ApplicationId))
|
||||||
{
|
{
|
||||||
Device.Dispose();
|
Device.Dispose();
|
||||||
|
|
||||||
@@ -710,7 +717,7 @@ namespace Ryujinx.Ava
|
|||||||
|
|
||||||
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
|
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata =>
|
||||||
{
|
{
|
||||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
appMetadata.UpdatePreGame();
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
"MenuBarFileOpenEmuFolder": "Open Ryujinx Folder",
|
"MenuBarFileOpenEmuFolder": "Open Ryujinx Folder",
|
||||||
"MenuBarFileOpenLogsFolder": "Open Logs Folder",
|
"MenuBarFileOpenLogsFolder": "Open Logs Folder",
|
||||||
"MenuBarFileExit": "_Exit",
|
"MenuBarFileExit": "_Exit",
|
||||||
"MenuBarOptions": "Options",
|
"MenuBarOptions": "_Options",
|
||||||
"MenuBarOptionsToggleFullscreen": "Toggle Fullscreen",
|
"MenuBarOptionsToggleFullscreen": "Toggle Fullscreen",
|
||||||
"MenuBarOptionsStartGamesInFullscreen": "Start Games in Fullscreen Mode",
|
"MenuBarOptionsStartGamesInFullscreen": "Start Games in Fullscreen Mode",
|
||||||
"MenuBarOptionsStopEmulation": "Stop Emulation",
|
"MenuBarOptionsStopEmulation": "Stop Emulation",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"MenuBarToolsManageFileTypes": "Manage file types",
|
"MenuBarToolsManageFileTypes": "Manage file types",
|
||||||
"MenuBarToolsInstallFileTypes": "Install file types",
|
"MenuBarToolsInstallFileTypes": "Install file types",
|
||||||
"MenuBarToolsUninstallFileTypes": "Uninstall file types",
|
"MenuBarToolsUninstallFileTypes": "Uninstall file types",
|
||||||
"MenuBarHelp": "Help",
|
"MenuBarHelp": "_Help",
|
||||||
"MenuBarHelpCheckForUpdates": "Check for Updates",
|
"MenuBarHelpCheckForUpdates": "Check for Updates",
|
||||||
"MenuBarHelpAbout": "About",
|
"MenuBarHelpAbout": "About",
|
||||||
"MenuSearch": "Search...",
|
"MenuSearch": "Search...",
|
||||||
@@ -72,6 +72,8 @@
|
|||||||
"GameListContextMenuExtractDataRomFSToolTip": "Extract the RomFS section from Application's current config (including updates)",
|
"GameListContextMenuExtractDataRomFSToolTip": "Extract the RomFS section from Application's current config (including updates)",
|
||||||
"GameListContextMenuExtractDataLogo": "Logo",
|
"GameListContextMenuExtractDataLogo": "Logo",
|
||||||
"GameListContextMenuExtractDataLogoToolTip": "Extract the Logo section from Application's current config (including updates)",
|
"GameListContextMenuExtractDataLogoToolTip": "Extract the Logo section from Application's current config (including updates)",
|
||||||
|
"GameListContextMenuCreateShortcut": "Create Application Shortcut",
|
||||||
|
"GameListContextMenuCreateShortcutToolTip": "Create a Desktop Shortcut that launches the selected Application",
|
||||||
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
|
"StatusBarGamesLoaded": "{0}/{1} Games Loaded",
|
||||||
"StatusBarSystemVersion": "System Version: {0}",
|
"StatusBarSystemVersion": "System Version: {0}",
|
||||||
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
|
"LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected",
|
||||||
@@ -537,6 +539,8 @@
|
|||||||
"OpenSetupGuideMessage": "Open the Setup Guide",
|
"OpenSetupGuideMessage": "Open the Setup Guide",
|
||||||
"NoUpdate": "No Update",
|
"NoUpdate": "No Update",
|
||||||
"TitleUpdateVersionLabel": "Version {0}",
|
"TitleUpdateVersionLabel": "Version {0}",
|
||||||
|
"TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
|
||||||
|
"TitleBundledDlcLabel": "Bundled:",
|
||||||
"RyujinxInfo": "Ryujinx - Info",
|
"RyujinxInfo": "Ryujinx - Info",
|
||||||
"RyujinxConfirm": "Ryujinx - Confirmation",
|
"RyujinxConfirm": "Ryujinx - Confirmation",
|
||||||
"FileDialogAllTypes": "All types",
|
"FileDialogAllTypes": "All types",
|
||||||
@@ -648,7 +652,7 @@
|
|||||||
"UserEditorTitle": "Edit User",
|
"UserEditorTitle": "Edit User",
|
||||||
"UserEditorTitleCreate": "Create User",
|
"UserEditorTitleCreate": "Create User",
|
||||||
"SettingsTabNetworkInterface": "Network Interface:",
|
"SettingsTabNetworkInterface": "Network Interface:",
|
||||||
"NetworkInterfaceTooltip": "The network interface used for LAN features",
|
"NetworkInterfaceTooltip": "The network interface used for LAN/LDN features",
|
||||||
"NetworkInterfaceDefault": "Default",
|
"NetworkInterfaceDefault": "Default",
|
||||||
"PackagingShaders": "Packaging Shaders",
|
"PackagingShaders": "Packaging Shaders",
|
||||||
"AboutChangelogButton": "View Changelog on GitHub",
|
"AboutChangelogButton": "View Changelog on GitHub",
|
||||||
|
@@ -18,7 +18,8 @@ using Ryujinx.Ava.UI.Helpers;
|
|||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
@@ -173,7 +174,7 @@ namespace Ryujinx.Ava.Common
|
|||||||
string extension = Path.GetExtension(titleFilePath).ToLower();
|
string extension = Path.GetExtension(titleFilePath).ToLower();
|
||||||
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
|
if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
|
||||||
{
|
{
|
||||||
PartitionFileSystem pfs;
|
IFileSystem pfs;
|
||||||
|
|
||||||
if (extension == ".xci")
|
if (extension == ".xci")
|
||||||
{
|
{
|
||||||
@@ -181,7 +182,9 @@ namespace Ryujinx.Ava.Common
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
pfs = new PartitionFileSystem(file.AsStorage());
|
var pfsTemp = new PartitionFileSystem();
|
||||||
|
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||||
|
pfs = pfsTemp;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
@@ -224,7 +227,11 @@ namespace Ryujinx.Ava.Common
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
|
(Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
|
||||||
if (updatePatchNca != null)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
patchNca = updatePatchNca;
|
patchNca = updatePatchNca;
|
||||||
|
@@ -6,13 +6,13 @@ using Ryujinx.Common;
|
|||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.GraphicsDriver;
|
using Ryujinx.Common.GraphicsDriver;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.SystemInfo;
|
|
||||||
using Ryujinx.Common.SystemInterop;
|
using Ryujinx.Common.SystemInterop;
|
||||||
using Ryujinx.Modules;
|
using Ryujinx.Modules;
|
||||||
using Ryujinx.SDL2.Common;
|
using Ryujinx.SDL2.Common;
|
||||||
using Ryujinx.Ui.Common;
|
using Ryujinx.Ui.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
|
using Ryujinx.Ui.Common.SystemInfo;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
@@ -145,4 +145,4 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Include="Assets\Locales\en_US.json" />
|
<AdditionalFiles Include="Assets\Locales\en_US.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@@ -12,6 +12,11 @@
|
|||||||
Click="ToggleFavorite_Click"
|
Click="ToggleFavorite_Click"
|
||||||
Header="{locale:Locale GameListContextMenuToggleFavorite}"
|
Header="{locale:Locale GameListContextMenuToggleFavorite}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuToggleFavoriteToolTip}" />
|
||||||
|
<MenuItem
|
||||||
|
Click="CreateApplicationShortcut_Click"
|
||||||
|
Header="{locale:Locale GameListContextMenuCreateShortcut}"
|
||||||
|
IsEnabled="{Binding CreateShortcutEnabled}"
|
||||||
|
ToolTip.Tip="{locale:Locale GameListContextMenuCreateShortcutToolTip}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Click="OpenUserSaveDirectory_Click"
|
Click="OpenUserSaveDirectory_Click"
|
||||||
@@ -82,4 +87,4 @@
|
|||||||
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
|
Header="{locale:Locale GameListContextMenuExtractDataLogo}"
|
||||||
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
|
ToolTip.Tip="{locale:Locale GameListContextMenuExtractDataLogoToolTip}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Threading;
|
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
@@ -15,7 +14,6 @@ using Ryujinx.Ui.App.Common;
|
|||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
@@ -41,7 +39,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
|
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
|
||||||
|
|
||||||
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata =>
|
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata =>
|
||||||
{
|
{
|
||||||
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
|
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
|
||||||
});
|
});
|
||||||
@@ -76,19 +74,9 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
|
var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default);
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name);
|
||||||
}
|
|
||||||
|
|
||||||
var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
|
|
||||||
|
|
||||||
ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
|
await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
|
await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +108,8 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
await new CheatWindow(
|
await new CheatWindow(
|
||||||
viewModel.VirtualFileSystem,
|
viewModel.VirtualFileSystem,
|
||||||
viewModel.SelectedApplication.TitleId,
|
viewModel.SelectedApplication.IdString,
|
||||||
viewModel.SelectedApplication.TitleName,
|
viewModel.SelectedApplication.Name,
|
||||||
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
|
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,7 +121,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string modsBasePath = ModLoader.GetModsBasePath();
|
string modsBasePath = ModLoader.GetModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.TitleId);
|
string titleModsPath = ModLoader.GetTitleDir(modsBasePath, viewModel.SelectedApplication.IdString);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
@@ -146,7 +134,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
||||||
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.TitleId);
|
string titleModsPath = ModLoader.GetTitleDir(sdModsBasePath, viewModel.SelectedApplication.IdString);
|
||||||
|
|
||||||
OpenHelper.OpenFolder(titleModsPath);
|
OpenHelper.OpenFolder(titleModsPath);
|
||||||
}
|
}
|
||||||
@@ -160,15 +148,15 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
||||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName),
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name),
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
|
|
||||||
if (result == UserResult.Yes)
|
if (result == UserResult.Yes)
|
||||||
{
|
{
|
||||||
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0"));
|
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0"));
|
||||||
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1"));
|
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1"));
|
||||||
|
|
||||||
List<FileInfo> cacheFiles = new();
|
List<FileInfo> cacheFiles = new();
|
||||||
|
|
||||||
@@ -208,14 +196,14 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
{
|
{
|
||||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||||
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
LocaleManager.Instance[LocaleKeys.DialogWarning],
|
||||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName),
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name),
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||||
|
|
||||||
if (result == UserResult.Yes)
|
if (result == UserResult.Yes)
|
||||||
{
|
{
|
||||||
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"));
|
DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"));
|
||||||
|
|
||||||
List<DirectoryInfo> oldCacheDirectories = new();
|
List<DirectoryInfo> oldCacheDirectories = new();
|
||||||
List<FileInfo> newCacheFiles = new();
|
List<FileInfo> newCacheFiles = new();
|
||||||
@@ -263,7 +251,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu");
|
string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu");
|
||||||
string mainDir = Path.Combine(ptcDir, "0");
|
string mainDir = Path.Combine(ptcDir, "0");
|
||||||
string backupDir = Path.Combine(ptcDir, "1");
|
string backupDir = Path.Combine(ptcDir, "1");
|
||||||
|
|
||||||
@@ -284,7 +272,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader");
|
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader");
|
||||||
|
|
||||||
if (!Directory.Exists(shaderCacheDir))
|
if (!Directory.Exists(shaderCacheDir))
|
||||||
{
|
{
|
||||||
@@ -305,7 +293,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
viewModel.StorageProvider,
|
viewModel.StorageProvider,
|
||||||
NcaSectionType.Code,
|
NcaSectionType.Code,
|
||||||
viewModel.SelectedApplication.Path,
|
viewModel.SelectedApplication.Path,
|
||||||
viewModel.SelectedApplication.TitleName);
|
viewModel.SelectedApplication.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +307,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
viewModel.StorageProvider,
|
viewModel.StorageProvider,
|
||||||
NcaSectionType.Data,
|
NcaSectionType.Data,
|
||||||
viewModel.SelectedApplication.Path,
|
viewModel.SelectedApplication.Path,
|
||||||
viewModel.SelectedApplication.TitleName);
|
viewModel.SelectedApplication.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +321,18 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
viewModel.StorageProvider,
|
viewModel.StorageProvider,
|
||||||
NcaSectionType.Logo,
|
NcaSectionType.Logo,
|
||||||
viewModel.SelectedApplication.Path,
|
viewModel.SelectedApplication.Path,
|
||||||
viewModel.SelectedApplication.TitleName);
|
viewModel.SelectedApplication.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
var viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
|
||||||
|
|
||||||
|
if (viewModel?.SelectedApplication != null)
|
||||||
|
{
|
||||||
|
ApplicationData selectedApplication = viewModel.SelectedApplication;
|
||||||
|
ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +342,7 @@ namespace Ryujinx.Ava.UI.Controls
|
|||||||
|
|
||||||
if (viewModel?.SelectedApplication != null)
|
if (viewModel?.SelectedApplication != null)
|
||||||
{
|
{
|
||||||
await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
|
await viewModel.LoadApplication(viewModel.SelectedApplication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -82,7 +82,7 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="{Binding TitleName}"
|
Text="{Binding Name}"
|
||||||
TextAlignment="Center"
|
TextAlignment="Center"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
@@ -85,7 +85,7 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
Text="{Binding TitleName}"
|
Text="{Binding Name}"
|
||||||
TextAlignment="Left"
|
TextAlignment="Left"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
Spacing="5">
|
Spacing="5">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Text="{Binding TitleId}"
|
Text="{Binding Id, StringFormat=X16}"
|
||||||
TextAlignment="Left"
|
TextAlignment="Left"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -126,17 +126,17 @@
|
|||||||
Spacing="5">
|
Spacing="5">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Text="{Binding TimePlayed}"
|
Text="{Binding TimePlayedString}"
|
||||||
TextAlignment="Right"
|
TextAlignment="Right"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Text="{Binding LastPlayed, Converter={helpers:NullableDateTimeConverter}}"
|
Text="{Binding LastPlayedString, Converter={helpers:LocalizedNeverConverter}}"
|
||||||
TextAlignment="Right"
|
TextAlignment="Right"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Text="{Binding FileSize}"
|
Text="{Binding FileSizeString}"
|
||||||
TextAlignment="Right"
|
TextAlignment="Right"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
43
src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
Normal file
43
src/Ryujinx.Ava/UI/Helpers/LocalizedNeverConverter.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ui.Common.Helper;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This <see cref="IValueConverter"/> makes sure that the string "Never" that's returned by <see cref="ValueFormatUtils.FormatDateTime"/> is properly localized in the Avalonia UI.
|
||||||
|
/// After the Avalonia UI has been made the default and the GTK UI is removed, <see cref="ValueFormatUtils"/> should be updated to directly return a localized string.
|
||||||
|
/// </summary>
|
||||||
|
internal class LocalizedNeverConverter : MarkupExtension, IValueConverter
|
||||||
|
{
|
||||||
|
private static readonly LocalizedNeverConverter _instance = new();
|
||||||
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is not string valStr)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valStr == "Never")
|
||||||
|
{
|
||||||
|
return LocaleManager.Instance[LocaleKeys.Never];
|
||||||
|
}
|
||||||
|
|
||||||
|
return valStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,38 +0,0 @@
|
|||||||
using Avalonia.Data.Converters;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Ryujinx.Ava.Common.Locale;
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
|
||||||
{
|
|
||||||
internal class NullableDateTimeConverter : MarkupExtension, IValueConverter
|
|
||||||
{
|
|
||||||
private static readonly NullableDateTimeConverter _instance = new();
|
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
return LocaleManager.Instance[LocaleKeys.Never];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value is DateTime dateTime)
|
|
||||||
{
|
|
||||||
return dateTime.ToLocalTime().ToString(culture);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
return _instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,4 +1,5 @@
|
|||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Models
|
namespace Ryujinx.Ava.UI.Models
|
||||||
@@ -24,6 +25,9 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
|
|
||||||
public string FileName => Path.GetFileName(ContainerPath);
|
public string FileName => Path.GetFileName(ContainerPath);
|
||||||
|
|
||||||
|
public string Label =>
|
||||||
|
Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName;
|
||||||
|
|
||||||
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
|
||||||
{
|
{
|
||||||
TitleId = titleId;
|
TitleId = titleId;
|
||||||
|
@@ -13,20 +13,19 @@ namespace Ryujinx.Ava.UI.Models.Generic
|
|||||||
|
|
||||||
public int Compare(ApplicationData x, ApplicationData y)
|
public int Compare(ApplicationData x, ApplicationData y)
|
||||||
{
|
{
|
||||||
var aValue = x.LastPlayed;
|
DateTime aValue = DateTime.UnixEpoch, bValue = DateTime.UnixEpoch;
|
||||||
var bValue = y.LastPlayed;
|
|
||||||
|
|
||||||
if (!aValue.HasValue)
|
if (x?.LastPlayed != null)
|
||||||
{
|
{
|
||||||
aValue = DateTime.UnixEpoch;
|
aValue = x.LastPlayed.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bValue.HasValue)
|
if (y?.LastPlayed != null)
|
||||||
{
|
{
|
||||||
bValue = DateTime.UnixEpoch;
|
bValue = y.LastPlayed.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (IsAscending ? 1 : -1) * DateTime.Compare(bValue.Value, aValue.Value);
|
return (IsAscending ? 1 : -1) * DateTime.Compare(aValue, bValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
Normal file
31
src/Ryujinx.Ava/UI/Models/Generic/TimePlayedSortComparer.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using Ryujinx.Ui.App.Common;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Models.Generic
|
||||||
|
{
|
||||||
|
internal class TimePlayedSortComparer : IComparer<ApplicationData>
|
||||||
|
{
|
||||||
|
public TimePlayedSortComparer() { }
|
||||||
|
public TimePlayedSortComparer(bool isAscending) { IsAscending = isAscending; }
|
||||||
|
|
||||||
|
public bool IsAscending { get; }
|
||||||
|
|
||||||
|
public int Compare(ApplicationData x, ApplicationData y)
|
||||||
|
{
|
||||||
|
TimeSpan aValue = TimeSpan.Zero, bValue = TimeSpan.Zero;
|
||||||
|
|
||||||
|
if (x?.TimePlayed != null)
|
||||||
|
{
|
||||||
|
aValue = x.TimePlayed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y?.TimePlayed != null)
|
||||||
|
{
|
||||||
|
bValue = y.TimePlayed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (IsAscending ? 1 : -1) * TimeSpan.Compare(aValue, bValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@ using Ryujinx.Ava.UI.ViewModels;
|
|||||||
using Ryujinx.Ava.UI.Windows;
|
using Ryujinx.Ava.UI.Windows;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
using System;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -38,26 +38,7 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
|
|
||||||
public bool SizeAvailable { get; set; }
|
public bool SizeAvailable { get; set; }
|
||||||
|
|
||||||
public string SizeString => GetSizeString();
|
public string SizeString => ValueFormatUtils.FormatFileSize(Size);
|
||||||
|
|
||||||
private string GetSizeString()
|
|
||||||
{
|
|
||||||
const int Scale = 1024;
|
|
||||||
string[] orders = { "GiB", "MiB", "KiB" };
|
|
||||||
long max = (long)Math.Pow(Scale, orders.Length);
|
|
||||||
|
|
||||||
foreach (string order in orders)
|
|
||||||
{
|
|
||||||
if (Size > max)
|
|
||||||
{
|
|
||||||
return $"{decimal.Divide(Size, max):##.##} {order}";
|
|
||||||
}
|
|
||||||
|
|
||||||
max /= Scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "0 KiB";
|
|
||||||
}
|
|
||||||
|
|
||||||
public SaveModel(SaveDataInfo info)
|
public SaveModel(SaveDataInfo info)
|
||||||
{
|
{
|
||||||
@@ -65,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
TitleId = info.ProgramId;
|
TitleId = info.ProgramId;
|
||||||
UserId = info.UserId;
|
UserId = info.UserId;
|
||||||
|
|
||||||
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString);
|
var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString);
|
||||||
|
|
||||||
InGameList = appData != null;
|
InGameList = appData != null;
|
||||||
|
|
||||||
if (InGameList)
|
if (InGameList)
|
||||||
{
|
{
|
||||||
Icon = appData.Icon;
|
Icon = appData.Icon;
|
||||||
Title = appData.TitleName;
|
Title = appData.Name;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@@ -8,7 +8,10 @@ namespace Ryujinx.Ava.UI.Models
|
|||||||
public ApplicationControlProperty Control { get; }
|
public ApplicationControlProperty Control { get; }
|
||||||
public string Path { get; }
|
public string Path { get; }
|
||||||
|
|
||||||
public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString());
|
public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||||
|
System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel,
|
||||||
|
Control.DisplayVersionString.ToString()
|
||||||
|
);
|
||||||
|
|
||||||
public TitleUpdateModel(ApplicationControlProperty control, string path)
|
public TitleUpdateModel(ApplicationControlProperty control, string path)
|
||||||
{
|
{
|
||||||
|
@@ -17,11 +17,12 @@ using Ryujinx.Common.Configuration;
|
|||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
|
using Ryujinx.Ui.App.Common;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Application = Avalonia.Application;
|
using Application = Avalonia.Application;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
private AvaloniaList<DownloadableContentModel> _selectedDownloadableContents = new();
|
||||||
|
|
||||||
private string _search;
|
private string _search;
|
||||||
private readonly ulong _titleId;
|
private readonly ApplicationData _applicationData;
|
||||||
|
|
||||||
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
|
||||||
@@ -92,18 +93,25 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public IStorageProvider StorageProvider;
|
public IStorageProvider StorageProvider;
|
||||||
|
|
||||||
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
|
public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
_virtualFileSystem = virtualFileSystem;
|
_virtualFileSystem = virtualFileSystem;
|
||||||
|
|
||||||
_titleId = titleId;
|
_applicationData = applicationData;
|
||||||
|
|
||||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
StorageProvider = desktop.MainWindow.StorageProvider;
|
StorageProvider = desktop.MainWindow.StorageProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
|
_downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json");
|
||||||
|
|
||||||
|
if (!File.Exists(_downloadableContentJsonPath))
|
||||||
|
{
|
||||||
|
_downloadableContentContainerList = new List<DownloadableContentContainer>();
|
||||||
|
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -120,13 +128,21 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
private void LoadDownloadableContents()
|
private void LoadDownloadableContents()
|
||||||
{
|
{
|
||||||
|
// NOTE: Try to load downloadable contents from PFS first.
|
||||||
|
AddDownloadableContent(_applicationData.Path);
|
||||||
|
|
||||||
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
foreach (DownloadableContentContainer downloadableContentContainer in _downloadableContentContainerList)
|
||||||
{
|
{
|
||||||
if (File.Exists(downloadableContentContainer.ContainerPath))
|
if (File.Exists(downloadableContentContainer.ContainerPath))
|
||||||
{
|
{
|
||||||
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
|
||||||
|
|
||||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
PartitionFileSystem partitionFileSystem = new();
|
||||||
|
|
||||||
|
if (partitionFileSystem.Initialize(containerFile.AsStorage()).IsFailure())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||||
|
|
||||||
@@ -219,21 +235,34 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
foreach (var file in result)
|
foreach (var file in result)
|
||||||
{
|
{
|
||||||
await AddDownloadableContent(file.Path.LocalPath);
|
if (!AddDownloadableContent(file.Path.LocalPath))
|
||||||
|
{
|
||||||
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddDownloadableContent(string path)
|
private bool AddDownloadableContent(string path)
|
||||||
{
|
{
|
||||||
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
|
||||||
{
|
{
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
using FileStream containerFile = File.OpenRead(path);
|
using FileStream containerFile = File.OpenRead(path);
|
||||||
|
|
||||||
PartitionFileSystem partitionFileSystem = new(containerFile.AsStorage());
|
IFileSystem partitionFileSystem;
|
||||||
bool containsDownloadableContent = false;
|
|
||||||
|
if (Path.GetExtension(path).ToLower() == ".xci")
|
||||||
|
{
|
||||||
|
partitionFileSystem = new Xci(_virtualFileSystem.KeySet, containerFile.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var pfsTemp = new PartitionFileSystem();
|
||||||
|
pfsTemp.Initialize(containerFile.AsStorage()).ThrowIfFailure();
|
||||||
|
partitionFileSystem = pfsTemp;
|
||||||
|
}
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||||
|
|
||||||
@@ -251,7 +280,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||||
{
|
{
|
||||||
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
|
if (nca.GetProgramIdBase() != _applicationData.IdBase)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -263,14 +292,11 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
OnPropertyChanged(nameof(UpdateCount));
|
OnPropertyChanged(nameof(UpdateCount));
|
||||||
Sort();
|
Sort();
|
||||||
|
|
||||||
containsDownloadableContent = true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!containsDownloadableContent)
|
return false;
|
||||||
{
|
|
||||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(DownloadableContentModel model)
|
public void Remove(DownloadableContentModel model)
|
||||||
|
@@ -95,7 +95,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
private bool _canUpdate = true;
|
private bool _canUpdate = true;
|
||||||
private Cursor _cursor;
|
private Cursor _cursor;
|
||||||
private string _title;
|
private string _title;
|
||||||
private string _currentEmulatedGamePath;
|
private ApplicationData _currentApplicationData;
|
||||||
private readonly AutoResetEvent _rendererWaitEvent;
|
private readonly AutoResetEvent _rendererWaitEvent;
|
||||||
private WindowState _windowState;
|
private WindowState _windowState;
|
||||||
private double _windowWidth;
|
private double _windowWidth;
|
||||||
@@ -106,7 +106,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
public ApplicationData ListSelectedApplication;
|
public ApplicationData ListSelectedApplication;
|
||||||
public ApplicationData GridSelectedApplication;
|
public ApplicationData GridSelectedApplication;
|
||||||
|
|
||||||
private string TitleName { get; set; }
|
|
||||||
internal AppHost AppHost { get; set; }
|
internal AppHost AppHost { get; set; }
|
||||||
|
|
||||||
public MainWindowViewModel()
|
public MainWindowViewModel()
|
||||||
@@ -356,6 +355,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||||
|
|
||||||
|
public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild();
|
||||||
|
|
||||||
public string LoadHeading
|
public string LoadHeading
|
||||||
{
|
{
|
||||||
get => _loadHeading;
|
get => _loadHeading;
|
||||||
@@ -928,21 +929,20 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
return SortMode switch
|
return SortMode switch
|
||||||
{
|
{
|
||||||
#pragma warning disable IDE0055 // Disable formatting
|
#pragma warning disable IDE0055 // Disable formatting
|
||||||
|
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Name)
|
||||||
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.Name),
|
||||||
|
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
||||||
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
||||||
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
|
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
|
||||||
ApplicationSort.FileSize => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileSizeBytes)
|
ApplicationSort.TotalTimePlayed => new TimePlayedSortComparer(IsAscending),
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileSizeBytes),
|
ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension)
|
||||||
ApplicationSort.TotalTimePlayed => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TimePlayedNum)
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileExtension),
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.TimePlayedNum),
|
ApplicationSort.FileSize => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileSize)
|
||||||
ApplicationSort.Title => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.TitleName)
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileSize),
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.TitleName),
|
ApplicationSort.Path => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path)
|
||||||
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.Path),
|
||||||
ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Favorite)
|
ApplicationSort.Favorite => !IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Favorite)
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Favorite),
|
: SortExpressionComparer<ApplicationData>.Descending(app => app.Favorite),
|
||||||
ApplicationSort.Developer => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Developer)
|
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Developer),
|
|
||||||
ApplicationSort.FileType => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.FileExtension)
|
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.FileExtension),
|
|
||||||
ApplicationSort.Path => IsAscending ? SortExpressionComparer<ApplicationData>.Ascending(app => app.Path)
|
|
||||||
: SortExpressionComparer<ApplicationData>.Descending(app => app.Path),
|
|
||||||
_ => null,
|
_ => null,
|
||||||
#pragma warning restore IDE0055
|
#pragma warning restore IDE0055
|
||||||
};
|
};
|
||||||
@@ -967,7 +967,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
if (arg is ApplicationData app)
|
if (arg is ApplicationData app)
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(_searchText) || app.TitleName.ToLower().Contains(_searchText.ToLower());
|
return string.IsNullOrWhiteSpace(_searchText) || app.Name.ToLower().Contains(_searchText.ToLower());
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -1096,7 +1096,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
IsLoadingIndeterminate = false;
|
IsLoadingIndeterminate = false;
|
||||||
break;
|
break;
|
||||||
case LoadState.Loaded:
|
case LoadState.Loaded:
|
||||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
|
||||||
IsLoadingIndeterminate = true;
|
IsLoadingIndeterminate = true;
|
||||||
CacheLoadStatus = "";
|
CacheLoadStatus = "";
|
||||||
break;
|
break;
|
||||||
@@ -1116,7 +1116,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
IsLoadingIndeterminate = false;
|
IsLoadingIndeterminate = false;
|
||||||
break;
|
break;
|
||||||
case ShaderCacheLoadingState.Loaded:
|
case ShaderCacheLoadingState.Loaded:
|
||||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
|
||||||
IsLoadingIndeterminate = true;
|
IsLoadingIndeterminate = true;
|
||||||
CacheLoadStatus = "";
|
CacheLoadStatus = "";
|
||||||
break;
|
break;
|
||||||
@@ -1167,13 +1167,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
UserChannelPersistence.ShouldRestart = false;
|
UserChannelPersistence.ShouldRestart = false;
|
||||||
|
|
||||||
await LoadApplication(_currentEmulatedGamePath);
|
await LoadApplication(_currentApplicationData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Otherwise, clear state.
|
// Otherwise, clear state.
|
||||||
UserChannelPersistence = new UserChannelPersistence();
|
UserChannelPersistence = new UserChannelPersistence();
|
||||||
_currentEmulatedGamePath = null;
|
_currentApplicationData = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1450,7 +1450,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
if (result.Count > 0)
|
if (result.Count > 0)
|
||||||
{
|
{
|
||||||
await LoadApplication(result[0].Path.LocalPath);
|
ApplicationData applicationData = new()
|
||||||
|
{
|
||||||
|
Path = result[0].Path.LocalPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
await LoadApplication(applicationData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1464,11 +1469,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
if (result.Count > 0)
|
if (result.Count > 0)
|
||||||
{
|
{
|
||||||
await LoadApplication(result[0].Path.LocalPath);
|
ApplicationData applicationData = new()
|
||||||
|
{
|
||||||
|
Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath),
|
||||||
|
Path = result[0].Path.LocalPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
await LoadApplication(applicationData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "")
|
public async Task LoadApplication(ApplicationData application, bool startFullscreen = false)
|
||||||
{
|
{
|
||||||
if (AppHost != null)
|
if (AppHost != null)
|
||||||
{
|
{
|
||||||
@@ -1488,7 +1499,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
Logger.RestartTime();
|
Logger.RestartTime();
|
||||||
|
|
||||||
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path);
|
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id);
|
||||||
|
|
||||||
PrepareLoadScreen();
|
PrepareLoadScreen();
|
||||||
|
|
||||||
@@ -1497,7 +1508,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
AppHost = new AppHost(
|
AppHost = new AppHost(
|
||||||
RendererHostControl,
|
RendererHostControl,
|
||||||
InputManager,
|
InputManager,
|
||||||
path,
|
application.Path,
|
||||||
|
application.Id,
|
||||||
VirtualFileSystem,
|
VirtualFileSystem,
|
||||||
ContentManager,
|
ContentManager,
|
||||||
AccountManager,
|
AccountManager,
|
||||||
@@ -1515,17 +1527,17 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
CanUpdate = false;
|
CanUpdate = false;
|
||||||
|
|
||||||
LoadHeading = TitleName = titleName;
|
LoadHeading = application.Name;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(titleName))
|
if (string.IsNullOrWhiteSpace(application.Name))
|
||||||
{
|
{
|
||||||
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
|
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
|
||||||
TitleName = AppHost.Device.Processes.ActiveApplication.Name;
|
application.Name = AppHost.Device.Processes.ActiveApplication.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
SwitchToRenderer(startFullscreen);
|
SwitchToRenderer(startFullscreen);
|
||||||
|
|
||||||
_currentEmulatedGamePath = path;
|
_currentApplicationData = application;
|
||||||
|
|
||||||
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
|
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
|
||||||
gameThread.Start();
|
gameThread.Start();
|
||||||
@@ -1547,13 +1559,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
{
|
{
|
||||||
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
|
ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
|
||||||
{
|
{
|
||||||
if (appMetadata.LastPlayed.HasValue)
|
appMetadata.UpdatePostGame();
|
||||||
{
|
|
||||||
double sessionTimePlayed = DateTime.UtcNow.Subtract(appMetadata.LastPlayed.Value).TotalSeconds;
|
|
||||||
appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
|
|
||||||
}
|
|
||||||
|
|
||||||
appMetadata.LastPlayed = DateTime.UtcNow;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1696,7 +1702,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
@@ -8,6 +7,7 @@ using LibHac.Fs;
|
|||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
@@ -17,12 +17,16 @@ using Ryujinx.Common.Configuration;
|
|||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Application = Avalonia.Application;
|
||||||
|
using ContentType = LibHac.Ncm.ContentType;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
using SpanHelpers = LibHac.Common.SpanHelpers;
|
using SpanHelpers = LibHac.Common.SpanHelpers;
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
public TitleUpdateMetadata TitleUpdateWindowData;
|
public TitleUpdateMetadata TitleUpdateWindowData;
|
||||||
public readonly string TitleUpdateJsonPath;
|
public readonly string TitleUpdateJsonPath;
|
||||||
private VirtualFileSystem VirtualFileSystem { get; }
|
private VirtualFileSystem VirtualFileSystem { get; }
|
||||||
private ulong TitleId { get; }
|
private ApplicationData ApplicationData { get; }
|
||||||
|
|
||||||
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
private AvaloniaList<TitleUpdateModel> _titleUpdates = new();
|
||||||
private AvaloniaList<object> _views = new();
|
private AvaloniaList<object> _views = new();
|
||||||
@@ -73,18 +77,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
public IStorageProvider StorageProvider;
|
public IStorageProvider StorageProvider;
|
||||||
|
|
||||||
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
|
public TitleUpdateViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
|
|
||||||
TitleId = titleId;
|
ApplicationData = applicationData;
|
||||||
|
|
||||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
StorageProvider = desktop.MainWindow.StorageProvider;
|
StorageProvider = desktop.MainWindow.StorageProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "updates.json");
|
TitleUpdateJsonPath = Path.Combine(AppDataManager.GamesDirPath, ApplicationData.IdString, "updates.json");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -92,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {TitleId} at {TitleUpdateJsonPath}");
|
Logger.Warning?.Print(LogClass.Application, $"Failed to deserialize title update data for {ApplicationData.IdString} at {TitleUpdateJsonPath}");
|
||||||
|
|
||||||
TitleUpdateWindowData = new TitleUpdateMetadata
|
TitleUpdateWindowData = new TitleUpdateMetadata
|
||||||
{
|
{
|
||||||
@@ -108,6 +112,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
private void LoadUpdates()
|
private void LoadUpdates()
|
||||||
{
|
{
|
||||||
|
// Try to load updates from PFS first
|
||||||
|
AddUpdate(ApplicationData.Path, true);
|
||||||
|
|
||||||
foreach (string path in TitleUpdateWindowData.Paths)
|
foreach (string path in TitleUpdateWindowData.Paths)
|
||||||
{
|
{
|
||||||
AddUpdate(path);
|
AddUpdate(path);
|
||||||
@@ -162,15 +169,41 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddUpdate(string path)
|
private void AddUpdate(string path, bool ignoreNotFound = false)
|
||||||
{
|
{
|
||||||
if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
|
if (File.Exists(path) && TitleUpdates.All(x => x.Path != path))
|
||||||
{
|
{
|
||||||
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
using FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
|
IFileSystem pfs;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
(Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(VirtualFileSystem, new PartitionFileSystem(file.AsStorage()), TitleId.ToString("x16"), 0);
|
if (Path.GetExtension(path).ToLower() == ".xci")
|
||||||
|
{
|
||||||
|
pfs = new Xci(VirtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var pfsTemp = new PartitionFileSystem();
|
||||||
|
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||||
|
pfs = pfsTemp;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<ulong, ContentCollection> updates = pfs.GetUpdateData(VirtualFileSystem, checkLevel);
|
||||||
|
|
||||||
|
Nca patchNca = null;
|
||||||
|
Nca controlNca = null;
|
||||||
|
|
||||||
|
if (updates.TryGetValue(ApplicationData.Id, out ContentCollection content))
|
||||||
|
{
|
||||||
|
patchNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Program);
|
||||||
|
controlNca = content.GetNcaByType(VirtualFileSystem.KeySet, ContentType.Control);
|
||||||
|
}
|
||||||
|
|
||||||
if (controlNca != null && patchNca != null)
|
if (controlNca != null && patchNca != null)
|
||||||
{
|
{
|
||||||
@@ -185,7 +218,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
if (!ignoreNotFound)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.InvokeAsync(() => ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUpdateAddUpdateErrorMessage]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@@ -10,6 +10,7 @@ using Ryujinx.Ava.UI.Windows;
|
|||||||
using Ryujinx.Common;
|
using Ryujinx.Common;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.Modules;
|
using Ryujinx.Modules;
|
||||||
|
using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common;
|
using Ryujinx.Ui.Common;
|
||||||
using Ryujinx.Ui.Common.Configuration;
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
@@ -131,7 +132,14 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(contentPath))
|
if (!string.IsNullOrEmpty(contentPath))
|
||||||
{
|
{
|
||||||
await ViewModel.LoadApplication(contentPath, false, "Mii Applet");
|
ApplicationData applicationData = new()
|
||||||
|
{
|
||||||
|
Name = "miiEdit",
|
||||||
|
Id = 0x0100000000001009,
|
||||||
|
Path = contentPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -104,7 +104,7 @@
|
|||||||
Content="{locale:Locale GameListHeaderApplication}"
|
Content="{locale:Locale GameListHeaderApplication}"
|
||||||
GroupName="Sort"
|
GroupName="Sort"
|
||||||
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
|
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
|
||||||
Tag="Title" />
|
Tag="Application" />
|
||||||
<RadioButton
|
<RadioButton
|
||||||
Checked="Sort_Checked"
|
Checked="Sort_Checked"
|
||||||
Content="{locale:Locale GameListHeaderDeveloper}"
|
Content="{locale:Locale GameListHeaderDeveloper}"
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.Ui.App.Common;
|
using Ryujinx.Ui.App.Common;
|
||||||
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -34,9 +36,12 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath)
|
public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath)
|
||||||
{
|
{
|
||||||
LoadedCheats = new AvaloniaList<CheatsList>();
|
LoadedCheats = new AvaloniaList<CheatsList>();
|
||||||
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper());
|
Heading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.CheatWindowHeading, titleName, titleId.ToUpper());
|
||||||
BuildId = ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath);
|
BuildId = ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath);
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
@@ -97,7 +97,7 @@
|
|||||||
MaxLines="2"
|
MaxLines="2"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
TextTrimming="CharacterEllipsis"
|
TextTrimming="CharacterEllipsis"
|
||||||
Text="{Binding FileName}" />
|
Text="{Binding Label}" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="10 0"
|
Margin="10 0"
|
||||||
|
@@ -7,9 +7,9 @@ using Ryujinx.Ava.UI.Helpers;
|
|||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Button = Avalonia.Controls.Button;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Windows
|
namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
@@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ulong titleId)
|
public DownloadableContentManagerWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, titleId);
|
DataContext = ViewModel = new DownloadableContentManagerViewModel(virtualFileSystem, applicationData);
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
ContentDialog contentDialog = new()
|
ContentDialog contentDialog = new()
|
||||||
{
|
{
|
||||||
PrimaryButtonText = "",
|
PrimaryButtonText = "",
|
||||||
SecondaryButtonText = "",
|
SecondaryButtonText = "",
|
||||||
CloseButtonText = "",
|
CloseButtonText = "",
|
||||||
Content = new DownloadableContentManagerWindow(virtualFileSystem, titleId),
|
Content = new DownloadableContentManagerWindow(virtualFileSystem, applicationData),
|
||||||
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], titleName, titleId.ToString("X16")),
|
Title = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowTitle], applicationData.Name, applicationData.IdString),
|
||||||
};
|
};
|
||||||
|
|
||||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||||
|
@@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using LibHac.Tools.FsSystem;
|
||||||
using Ryujinx.Ava.Common;
|
using Ryujinx.Ava.Common;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Input;
|
using Ryujinx.Ava.Input;
|
||||||
@@ -23,7 +24,6 @@ using Ryujinx.Ui.Common;
|
|||||||
using Ryujinx.Ui.Common.Configuration;
|
using Ryujinx.Ui.Common.Configuration;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -139,9 +139,7 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
{
|
{
|
||||||
ViewModel.SelectedIcon = args.Application.Icon;
|
ViewModel.SelectedIcon = args.Application.Icon;
|
||||||
|
|
||||||
string path = new FileInfo(args.Application.Path).FullName;
|
ViewModel.LoadApplication(args.Application).Wait();
|
||||||
|
|
||||||
ViewModel.LoadApplication(path).Wait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
@@ -190,7 +188,11 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
LibHacHorizonManager.InitializeBcatServer();
|
LibHacHorizonManager.InitializeBcatServer();
|
||||||
LibHacHorizonManager.InitializeSystemClients();
|
LibHacHorizonManager.InitializeSystemClients();
|
||||||
|
|
||||||
ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem);
|
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||||
|
? IntegrityCheckLevel.ErrorOnInvalid
|
||||||
|
: IntegrityCheckLevel.None;
|
||||||
|
|
||||||
|
ApplicationLibrary = new ApplicationLibrary(VirtualFileSystem, checkLevel);
|
||||||
|
|
||||||
// Save data created before we supported extra data in directory save data will not work properly if
|
// Save data created before we supported extra data in directory save data will not work properly if
|
||||||
// given empty extra data. Luckily some of that extra data can be created using the data from the
|
// given empty extra data. Luckily some of that extra data can be created using the data from the
|
||||||
@@ -297,7 +299,12 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
{
|
{
|
||||||
_deferLoad = false;
|
_deferLoad = false;
|
||||||
|
|
||||||
ViewModel.LoadApplication(_launchPath, _startFullscreen).Wait();
|
ApplicationData applicationData = new()
|
||||||
|
{
|
||||||
|
Path = _launchPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewModel.LoadApplication(applicationData, _startFullscreen).Wait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@@ -7,15 +7,15 @@ using Ryujinx.Ava.UI.Helpers;
|
|||||||
using Ryujinx.Ava.UI.Models;
|
using Ryujinx.Ava.UI.Models;
|
||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.Ui.App.Common;
|
||||||
using Ryujinx.Ui.Common.Helper;
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Button = Avalonia.Controls.Button;
|
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Windows
|
namespace Ryujinx.Ava.UI.Windows
|
||||||
{
|
{
|
||||||
public partial class TitleUpdateWindow : UserControl
|
public partial class TitleUpdateWindow : UserControl
|
||||||
{
|
{
|
||||||
public TitleUpdateViewModel ViewModel;
|
public readonly TitleUpdateViewModel ViewModel;
|
||||||
|
|
||||||
public TitleUpdateWindow()
|
public TitleUpdateWindow()
|
||||||
{
|
{
|
||||||
@@ -24,22 +24,22 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ulong titleId)
|
public TitleUpdateWindow(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, titleId);
|
DataContext = ViewModel = new TitleUpdateViewModel(virtualFileSystem, applicationData);
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task Show(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName)
|
public static async Task Show(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
|
||||||
{
|
{
|
||||||
ContentDialog contentDialog = new()
|
ContentDialog contentDialog = new()
|
||||||
{
|
{
|
||||||
PrimaryButtonText = "",
|
PrimaryButtonText = "",
|
||||||
SecondaryButtonText = "",
|
SecondaryButtonText = "",
|
||||||
CloseButtonText = "",
|
CloseButtonText = "",
|
||||||
Content = new TitleUpdateWindow(virtualFileSystem, titleId),
|
Content = new TitleUpdateWindow(virtualFileSystem, applicationData),
|
||||||
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, titleName, titleId.ToString("X16")),
|
Title = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.GameUpdateWindowHeading, applicationData.Name, applicationData.IdString),
|
||||||
};
|
};
|
||||||
|
|
||||||
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
Style bottomBorder = new(x => x.OfType<Grid>().Name("DialogSpace").Child().OfType<Border>());
|
||||||
|
@@ -3,5 +3,6 @@
|
|||||||
public enum MultiplayerMode
|
public enum MultiplayerMode
|
||||||
{
|
{
|
||||||
Disabled,
|
Disabled,
|
||||||
|
LdnMitm,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -74,5 +74,10 @@ namespace Ryujinx.Common.Utilities
|
|||||||
{
|
{
|
||||||
return ConvertIpv4Address(IPAddress.Parse(ipAddress));
|
return ConvertIpv4Address(IPAddress.Parse(ipAddress));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IPAddress ConvertUint(uint ipAddress)
|
||||||
|
{
|
||||||
|
return new IPAddress(new byte[] { (byte)((ipAddress >> 24) & 0xFF), (byte)((ipAddress >> 16) & 0xFF), (byte)((ipAddress >> 8) & 0xFF), (byte)(ipAddress & 0xFF) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs
Normal file
62
src/Ryujinx.Cpu/AppleHv/HvCodePatcher.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Intrinsics;
|
||||||
|
|
||||||
|
namespace Ryujinx.Cpu.AppleHv
|
||||||
|
{
|
||||||
|
static class HvCodePatcher
|
||||||
|
{
|
||||||
|
private const uint XMask = 0x3f808000u;
|
||||||
|
private const uint XValue = 0x8000000u;
|
||||||
|
|
||||||
|
private const uint ZrIndex = 31u;
|
||||||
|
|
||||||
|
public static void RewriteUnorderedExclusiveInstructions(Span<byte> code)
|
||||||
|
{
|
||||||
|
Span<uint> codeUint = MemoryMarshal.Cast<byte, uint>(code);
|
||||||
|
Span<Vector128<uint>> codeVector = MemoryMarshal.Cast<byte, Vector128<uint>>(code);
|
||||||
|
|
||||||
|
Vector128<uint> mask = Vector128.Create(XMask);
|
||||||
|
Vector128<uint> value = Vector128.Create(XValue);
|
||||||
|
|
||||||
|
for (int index = 0; index < codeVector.Length; index++)
|
||||||
|
{
|
||||||
|
Vector128<uint> v = codeVector[index];
|
||||||
|
|
||||||
|
if (Vector128.EqualsAny(Vector128.BitwiseAnd(v, mask), value))
|
||||||
|
{
|
||||||
|
int baseIndex = index * 4;
|
||||||
|
|
||||||
|
for (int instIndex = baseIndex; instIndex < baseIndex + 4; instIndex++)
|
||||||
|
{
|
||||||
|
ref uint inst = ref codeUint[instIndex];
|
||||||
|
|
||||||
|
if ((inst & XMask) != XValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPair = (inst & (1u << 21)) != 0;
|
||||||
|
bool isLoad = (inst & (1u << 22)) != 0;
|
||||||
|
|
||||||
|
uint rt2 = (inst >> 10) & 0x1fu;
|
||||||
|
uint rs = (inst >> 16) & 0x1fu;
|
||||||
|
|
||||||
|
if (isLoad && rs != ZrIndex)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPair && rt2 != ZrIndex)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the ordered flag.
|
||||||
|
inst |= 1u << 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -128,21 +128,6 @@ namespace Ryujinx.Cpu.AppleHv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable IDE0051 // Remove unused private member
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures the combination of virtual address and size is part of the addressable space and fully mapped.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="va">Virtual address of the range</param>
|
|
||||||
/// <param name="size">Size of the range in bytes</param>
|
|
||||||
private void AssertMapped(ulong va, ulong size)
|
|
||||||
{
|
|
||||||
if (!ValidateAddressAndSize(va, size) || !IsRangeMappedImpl(va, size))
|
|
||||||
{
|
|
||||||
throw new InvalidMemoryRegionException($"Not mapped: va=0x{va:X16}, size=0x{size:X16}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#pragma warning restore IDE0051
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
|
public void Map(ulong va, ulong pa, ulong size, MemoryMapFlags flags)
|
||||||
{
|
{
|
||||||
@@ -736,6 +721,24 @@ namespace Ryujinx.Cpu.AppleHv
|
|||||||
return (int)(vaSpan / PageSize);
|
return (int)(vaSpan / PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
|
{
|
||||||
|
if (protection.HasFlag(MemoryPermission.Execute))
|
||||||
|
{
|
||||||
|
// Some applications use unordered exclusive memory access instructions
|
||||||
|
// where it is not valid to do so, leading to memory re-ordering that
|
||||||
|
// makes the code behave incorrectly on some CPUs.
|
||||||
|
// To work around this, we force all such accesses to be ordered.
|
||||||
|
|
||||||
|
using WritableRegion writableRegion = GetWritableRegion(va, (int)size);
|
||||||
|
|
||||||
|
HvCodePatcher.RewriteUnorderedExclusiveInstructions(writableRegion.Memory.Span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
{
|
{
|
||||||
|
@@ -575,24 +575,17 @@ namespace Ryujinx.Cpu.Jit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable IDE0051 // Remove unused private member
|
|
||||||
private ulong GetPhysicalAddress(ulong va)
|
|
||||||
{
|
|
||||||
// We return -1L if the virtual address is invalid or unmapped.
|
|
||||||
if (!ValidateAddress(va) || !IsMapped(va))
|
|
||||||
{
|
|
||||||
return ulong.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetPhysicalAddressInternal(va);
|
|
||||||
}
|
|
||||||
#pragma warning restore IDE0051
|
|
||||||
|
|
||||||
private ulong GetPhysicalAddressInternal(ulong va)
|
private ulong GetPhysicalAddressInternal(ulong va)
|
||||||
{
|
{
|
||||||
return PteToPa(_pageTable.Read<ulong>((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask);
|
return PteToPa(_pageTable.Read<ulong>((va / PageSize) * PteSize) & ~(0xffffUL << 48)) + (va & PageMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
{
|
{
|
||||||
@@ -698,9 +691,5 @@ namespace Ryujinx.Cpu.Jit
|
|||||||
/// Disposes of resources used by the memory manager.
|
/// Disposes of resources used by the memory manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected override void Destroy() => _pageTable.Dispose();
|
protected override void Destroy() => _pageTable.Dispose();
|
||||||
|
|
||||||
#pragma warning disable IDE0051 // Remove unused private member
|
|
||||||
private static void ThrowInvalidMemoryRegionException(string message) => throw new InvalidMemoryRegionException(message);
|
|
||||||
#pragma warning restore IDE0051
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -615,6 +615,12 @@ namespace Ryujinx.Cpu.Jit
|
|||||||
return (int)(vaSpan / PageSize);
|
return (int)(vaSpan / PageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
{
|
{
|
||||||
|
@@ -101,6 +101,11 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AlwaysFlushOnOverlap { get; private set; }
|
public bool AlwaysFlushOnOverlap { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that the texture was fully unmapped since the modified flag was set, and flushes should be ignored until it is modified again.
|
||||||
|
/// </summary>
|
||||||
|
public bool FlushStale { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Increments when the host texture is swapped, or when the texture is removed from all pools.
|
/// Increments when the host texture is swapped, or when the texture is removed from all pools.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -149,6 +154,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HadPoolOwner { get; private set; }
|
public bool HadPoolOwner { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
/// Physical memory ranges where the texture data is located.
|
/// Physical memory ranges where the texture data is located.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MultiRange Range { get; private set; }
|
public MultiRange Range { get; private set; }
|
||||||
@@ -1411,6 +1417,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void SignalModified()
|
public void SignalModified()
|
||||||
{
|
{
|
||||||
|
FlushStale = false;
|
||||||
_scaledSetScore = Math.Max(0, _scaledSetScore - 1);
|
_scaledSetScore = Math.Max(0, _scaledSetScore - 1);
|
||||||
|
|
||||||
if (_modifiedStale || Group.HasCopyDependencies)
|
if (_modifiedStale || Group.HasCopyDependencies)
|
||||||
@@ -1431,6 +1438,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
if (bound)
|
if (bound)
|
||||||
{
|
{
|
||||||
|
FlushStale = false;
|
||||||
_scaledSetScore = Math.Max(0, _scaledSetScore - 1);
|
_scaledSetScore = Math.Max(0, _scaledSetScore - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1695,12 +1703,17 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
/// <param name="unmapRange">The range of memory being unmapped</param>
|
/// <param name="unmapRange">The range of memory being unmapped</param>
|
||||||
public void Unmapped(MultiRange unmapRange)
|
public void Unmapped(MultiRange unmapRange)
|
||||||
{
|
{
|
||||||
|
if (unmapRange.Contains(Range))
|
||||||
|
{
|
||||||
|
// If this is a full unmap, prevent flushes until the texture is mapped again.
|
||||||
|
FlushStale = true;
|
||||||
|
}
|
||||||
|
|
||||||
ChangedMapping = true;
|
ChangedMapping = true;
|
||||||
|
|
||||||
if (Group.Storage == this)
|
if (Group.Storage == this)
|
||||||
{
|
{
|
||||||
Group.Unmapped();
|
Group.Unmapped();
|
||||||
|
|
||||||
Group.ClearModified(unmapRange);
|
Group.ClearModified(unmapRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -107,8 +107,6 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
// Any texture that has been unmapped at any point or is partially unmapped
|
// Any texture that has been unmapped at any point or is partially unmapped
|
||||||
// should update their pool references after the remap completes.
|
// should update their pool references after the remap completes.
|
||||||
|
|
||||||
MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size);
|
|
||||||
|
|
||||||
foreach (var texture in _partiallyMappedTextures)
|
foreach (var texture in _partiallyMappedTextures)
|
||||||
{
|
{
|
||||||
texture.UpdatePoolMappings();
|
texture.UpdatePoolMappings();
|
||||||
@@ -735,9 +733,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
if (overlap.IsView)
|
if (overlap.IsView)
|
||||||
{
|
{
|
||||||
overlapCompatibility = overlapCompatibility == TextureViewCompatibility.FormatAlias ?
|
overlapCompatibility = TextureViewCompatibility.CopyOnly;
|
||||||
TextureViewCompatibility.Incompatible :
|
|
||||||
TextureViewCompatibility.CopyOnly;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -815,7 +811,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
Texture overlap = _textureOverlaps[index];
|
Texture overlap = _textureOverlaps[index];
|
||||||
OverlapInfo oInfo = _overlapInfo[index];
|
OverlapInfo oInfo = _overlapInfo[index];
|
||||||
|
|
||||||
if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible || oInfo.Compatibility == TextureViewCompatibility.FormatAlias)
|
if (oInfo.Compatibility <= TextureViewCompatibility.LayoutIncompatible)
|
||||||
{
|
{
|
||||||
if (!overlap.IsView && texture.DataOverlaps(overlap, oInfo.Compatibility))
|
if (!overlap.IsView && texture.DataOverlaps(overlap, oInfo.Compatibility))
|
||||||
{
|
{
|
||||||
|
@@ -226,7 +226,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
// D32F and R32F texture have the same representation internally,
|
// D32F and R32F texture have the same representation internally,
|
||||||
// however the R32F format is used to sample from depth textures.
|
// however the R32F format is used to sample from depth textures.
|
||||||
if (lhs.FormatInfo.Format == Format.D32Float && rhs.FormatInfo.Format == Format.R32Float && (forSampler || depthAlias))
|
if (IsValidDepthAsColorAlias(lhs.FormatInfo.Format, rhs.FormatInfo.Format) && (forSampler || depthAlias))
|
||||||
{
|
{
|
||||||
return TextureMatchQuality.FormatAlias;
|
return TextureMatchQuality.FormatAlias;
|
||||||
}
|
}
|
||||||
@@ -239,14 +239,8 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
{
|
{
|
||||||
return TextureMatchQuality.FormatAlias;
|
return TextureMatchQuality.FormatAlias;
|
||||||
}
|
}
|
||||||
|
else if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint ||
|
||||||
if (lhs.FormatInfo.Format == Format.D16Unorm && rhs.FormatInfo.Format == Format.R16Unorm)
|
lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm)
|
||||||
{
|
|
||||||
return TextureMatchQuality.FormatAlias;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((lhs.FormatInfo.Format == Format.D24UnormS8Uint ||
|
|
||||||
lhs.FormatInfo.Format == Format.S8UintD24Unorm) && rhs.FormatInfo.Format == Format.B8G8R8A8Unorm)
|
|
||||||
{
|
{
|
||||||
return TextureMatchQuality.FormatAlias;
|
return TextureMatchQuality.FormatAlias;
|
||||||
}
|
}
|
||||||
@@ -632,12 +626,27 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
|
|
||||||
if (lhsFormat.Format.IsDepthOrStencil() || rhsFormat.Format.IsDepthOrStencil())
|
if (lhsFormat.Format.IsDepthOrStencil() || rhsFormat.Format.IsDepthOrStencil())
|
||||||
{
|
{
|
||||||
return FormatMatches(lhs, rhs, flags.HasFlag(TextureSearchFlags.ForSampler), flags.HasFlag(TextureSearchFlags.DepthAlias)) switch
|
bool forSampler = flags.HasFlag(TextureSearchFlags.ForSampler);
|
||||||
|
bool depthAlias = flags.HasFlag(TextureSearchFlags.DepthAlias);
|
||||||
|
|
||||||
|
TextureMatchQuality matchQuality = FormatMatches(lhs, rhs, forSampler, depthAlias);
|
||||||
|
|
||||||
|
if (matchQuality == TextureMatchQuality.Perfect)
|
||||||
{
|
{
|
||||||
TextureMatchQuality.Perfect => TextureViewCompatibility.Full,
|
return TextureViewCompatibility.Full;
|
||||||
TextureMatchQuality.FormatAlias => TextureViewCompatibility.FormatAlias,
|
}
|
||||||
_ => TextureViewCompatibility.Incompatible,
|
else if (matchQuality == TextureMatchQuality.FormatAlias)
|
||||||
};
|
{
|
||||||
|
return TextureViewCompatibility.FormatAlias;
|
||||||
|
}
|
||||||
|
else if (IsValidColorAsDepthAlias(lhsFormat.Format, rhsFormat.Format) || IsValidDepthAsColorAlias(lhsFormat.Format, rhsFormat.Format))
|
||||||
|
{
|
||||||
|
return TextureViewCompatibility.CopyOnly;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return TextureViewCompatibility.Incompatible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsFormatHostIncompatible(lhs, caps) || IsFormatHostIncompatible(rhs, caps))
|
if (IsFormatHostIncompatible(lhs, caps) || IsFormatHostIncompatible(rhs, caps))
|
||||||
@@ -666,6 +675,30 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return TextureViewCompatibility.Incompatible;
|
return TextureViewCompatibility.Incompatible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if it's valid to alias a color format as a depth format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lhsFormat">Source format to be checked</param>
|
||||||
|
/// <param name="rhsFormat">Target format to be checked</param>
|
||||||
|
/// <returns>True if it's valid to alias the formats</returns>
|
||||||
|
private static bool IsValidColorAsDepthAlias(Format lhsFormat, Format rhsFormat)
|
||||||
|
{
|
||||||
|
return (lhsFormat == Format.R32Float && rhsFormat == Format.D32Float) ||
|
||||||
|
(lhsFormat == Format.R16Unorm && rhsFormat == Format.D16Unorm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if it's valid to alias a depth format as a color format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lhsFormat">Source format to be checked</param>
|
||||||
|
/// <param name="rhsFormat">Target format to be checked</param>
|
||||||
|
/// <returns>True if it's valid to alias the formats</returns>
|
||||||
|
private static bool IsValidDepthAsColorAlias(Format lhsFormat, Format rhsFormat)
|
||||||
|
{
|
||||||
|
return (lhsFormat == Format.D32Float && rhsFormat == Format.R32Float) ||
|
||||||
|
(lhsFormat == Format.D16Unorm && rhsFormat == Format.R16Unorm);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if aliasing of two formats that would normally be considered incompatible be allowed,
|
/// Checks if aliasing of two formats that would normally be considered incompatible be allowed,
|
||||||
/// using copy dependencies.
|
/// using copy dependencies.
|
||||||
|
@@ -1659,6 +1659,14 @@ namespace Ryujinx.Graphics.Gpu.Image
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If size is zero, we have nothing to flush.
|
||||||
|
// If the flush is stale, we should ignore it because the texture was unmapped since the modified
|
||||||
|
// flag was set, and flushing it is not safe anymore as the GPU might no longer own the memory.
|
||||||
|
if (size == 0 || Storage.FlushStale)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// There is a small gap here where the action is removed but _actionRegistered is still 1.
|
// There is a small gap here where the action is removed but _actionRegistered is still 1.
|
||||||
// In this case it will skip registering the action, but here we are already handling it,
|
// In this case it will skip registering the action, but here we are already handling it,
|
||||||
// so there shouldn't be any issue as it's the same handler for all actions.
|
// so there shouldn't be any issue as it's the same handler for all actions.
|
||||||
|
@@ -367,7 +367,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
|||||||
return to;
|
return to;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TextureView PboCopy(TextureView from, TextureView to, int srcLayer, int dstLayer, int srcLevel, int dstLevel, int width, int height)
|
public void PboCopy(TextureView from, TextureView to, int srcLayer, int dstLayer, int srcLevel, int dstLevel, int width, int height)
|
||||||
{
|
{
|
||||||
int dstWidth = width;
|
int dstWidth = width;
|
||||||
int dstHeight = height;
|
int dstHeight = height;
|
||||||
@@ -445,8 +445,6 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
GL.BindBuffer(BufferTarget.PixelUnpackBuffer, 0);
|
GL.BindBuffer(BufferTarget.PixelUnpackBuffer, 0);
|
||||||
|
|
||||||
return to;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsurePbo(TextureView view)
|
private void EnsurePbo(TextureView view)
|
||||||
|
@@ -140,6 +140,28 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
|||||||
int levels = Math.Min(Info.Levels, destinationView.Info.Levels - firstLevel);
|
int levels = Math.Min(Info.Levels, destinationView.Info.Levels - firstLevel);
|
||||||
_renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, 0, firstLayer, 0, firstLevel, layers, levels);
|
_renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, 0, firstLayer, 0, firstLevel, layers, levels);
|
||||||
}
|
}
|
||||||
|
else if (destinationView.Format.IsDepthOrStencil() != Format.IsDepthOrStencil())
|
||||||
|
{
|
||||||
|
int layers = Math.Min(Info.GetLayers(), destinationView.Info.GetLayers() - firstLayer);
|
||||||
|
int levels = Math.Min(Info.Levels, destinationView.Info.Levels - firstLevel);
|
||||||
|
|
||||||
|
for (int level = 0; level < levels; level++)
|
||||||
|
{
|
||||||
|
int srcWidth = Math.Max(1, Width >> level);
|
||||||
|
int srcHeight = Math.Max(1, Height >> level);
|
||||||
|
|
||||||
|
int dstWidth = Math.Max(1, destinationView.Width >> (firstLevel + level));
|
||||||
|
int dstHeight = Math.Max(1, destinationView.Height >> (firstLevel + level));
|
||||||
|
|
||||||
|
int minWidth = Math.Min(srcWidth, dstWidth);
|
||||||
|
int minHeight = Math.Min(srcHeight, dstHeight);
|
||||||
|
|
||||||
|
for (int layer = 0; layer < layers; layer++)
|
||||||
|
{
|
||||||
|
_renderer.TextureCopy.PboCopy(this, destinationView, 0, firstLayer + layer, 0, firstLevel + level, minWidth, minHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_renderer.TextureCopy.CopyUnscaled(this, destinationView, 0, firstLayer, 0, firstLevel);
|
_renderer.TextureCopy.CopyUnscaled(this, destinationView, 0, firstLayer, 0, firstLevel);
|
||||||
@@ -169,6 +191,13 @@ namespace Ryujinx.Graphics.OpenGL.Image
|
|||||||
{
|
{
|
||||||
_renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
_renderer.TextureCopyIncompatible.CopyIncompatibleFormats(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
||||||
}
|
}
|
||||||
|
else if (destinationView.Format.IsDepthOrStencil() != Format.IsDepthOrStencil())
|
||||||
|
{
|
||||||
|
int minWidth = Math.Min(Width, destinationView.Width);
|
||||||
|
int minHeight = Math.Min(Height, destinationView.Height);
|
||||||
|
|
||||||
|
_renderer.TextureCopy.PboCopy(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, minWidth, minHeight);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_renderer.TextureCopy.CopyUnscaled(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
_renderer.TextureCopy.CopyUnscaled(this, destinationView, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
||||||
|
@@ -766,7 +766,10 @@ namespace Ryujinx.Graphics.Shader.Instructions
|
|||||||
flags |= offset == TexOffset.Ptp ? TextureFlags.Offsets : TextureFlags.Offset;
|
flags |= offset == TexOffset.Ptp ? TextureFlags.Offsets : TextureFlags.Offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
sourcesList.Add(Const((int)component));
|
if (!hasDepthCompare)
|
||||||
|
{
|
||||||
|
sourcesList.Add(Const((int)component));
|
||||||
|
}
|
||||||
|
|
||||||
Operand[] sources = sourcesList.ToArray();
|
Operand[] sources = sourcesList.ToArray();
|
||||||
Operand[] dests = new Operand[BitOperations.PopCount((uint)componentMask)];
|
Operand[] dests = new Operand[BitOperations.PopCount((uint)componentMask)];
|
||||||
|
@@ -211,6 +211,13 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel);
|
int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel);
|
||||||
_gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, 0, firstLayer, 0, firstLevel, layers, levels);
|
_gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, 0, firstLayer, 0, firstLevel, layers, levels);
|
||||||
}
|
}
|
||||||
|
else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil())
|
||||||
|
{
|
||||||
|
int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer);
|
||||||
|
int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel);
|
||||||
|
|
||||||
|
_gd.HelperShader.CopyColor(_gd, cbs, src, dst, 0, firstLayer, 0, FirstLevel, layers, levels);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
TextureCopy.Copy(
|
TextureCopy.Copy(
|
||||||
@@ -260,6 +267,10 @@ namespace Ryujinx.Graphics.Vulkan
|
|||||||
{
|
{
|
||||||
_gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
_gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
||||||
}
|
}
|
||||||
|
else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil())
|
||||||
|
{
|
||||||
|
_gd.HelperShader.CopyColor(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
TextureCopy.Copy(
|
TextureCopy.Copy(
|
||||||
|
61
src/Ryujinx.HLE/FileSystem/ContentCollection.cs
Normal file
61
src/Ryujinx.HLE/FileSystem/ContentCollection.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using LibHac.Common.Keys;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.Ncm;
|
||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using LibHac.Tools.Ncm;
|
||||||
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.FileSystem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Thin wrapper around <see cref="Cnmt"/>
|
||||||
|
/// </summary>
|
||||||
|
public class ContentCollection
|
||||||
|
{
|
||||||
|
private readonly IFileSystem _pfs;
|
||||||
|
private readonly Cnmt _cnmt;
|
||||||
|
|
||||||
|
public ulong Id => _cnmt.TitleId;
|
||||||
|
public TitleVersion Version => _cnmt.TitleVersion;
|
||||||
|
public ContentMetaType Type => _cnmt.Type;
|
||||||
|
public ulong ApplicationId => _cnmt.ApplicationTitleId;
|
||||||
|
public ulong PatchId => _cnmt.PatchTitleId;
|
||||||
|
public TitleVersion RequiredSystemVersion => _cnmt.MinimumSystemVersion;
|
||||||
|
public TitleVersion RequiredApplicationVersion => _cnmt.MinimumApplicationVersion;
|
||||||
|
public byte[] Digest => _cnmt.Hash;
|
||||||
|
|
||||||
|
public ulong ProgramBaseId => Id & ~0x1FFFUL;
|
||||||
|
public bool IsSystemTitle => _cnmt.Type < ContentMetaType.Application;
|
||||||
|
|
||||||
|
public ContentCollection(IFileSystem pfs, Cnmt cnmt)
|
||||||
|
{
|
||||||
|
_pfs = pfs;
|
||||||
|
_cnmt = cnmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Nca GetNcaByType(KeySet keySet, ContentType type, int programIndex = 0)
|
||||||
|
{
|
||||||
|
// TODO: Replace this with a check for IdOffset as soon as LibHac supports it:
|
||||||
|
// && entry.IdOffset == programIndex
|
||||||
|
|
||||||
|
foreach (var entry in _cnmt.ContentEntries)
|
||||||
|
{
|
||||||
|
if (entry.Type != type)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string ncaId = BitConverter.ToString(entry.NcaId).Replace("-", null).ToLower();
|
||||||
|
Nca nca = _pfs.GetNca(keySet, $"/{ncaId}.nca");
|
||||||
|
|
||||||
|
if (nca.GetProgramIndex() == programIndex)
|
||||||
|
{
|
||||||
|
return nca;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -198,7 +198,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
{
|
{
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read);
|
fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
||||||
if (nca.Header.ContentType != NcaContentType.Meta)
|
if (nca.Header.ContentType != NcaContentType.Meta)
|
||||||
{
|
{
|
||||||
@@ -210,7 +210,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel);
|
using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel);
|
||||||
using var cnmtFile = new UniqueRef<IFile>();
|
using var cnmtFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read);
|
pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
var cnmt = new Cnmt(cnmtFile.Get.AsStream());
|
var cnmt = new Cnmt(cnmtFile.Get.AsStream());
|
||||||
if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId)
|
if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId)
|
||||||
@@ -220,7 +220,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
|
|
||||||
string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower();
|
string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower();
|
||||||
|
|
||||||
AddAocItem(cnmt.TitleId, containerPath, $"{ncaId}.nca", true);
|
AddAocItem(cnmt.TitleId, containerPath, $"/{ncaId}.nca", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +238,8 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
if (!mergedToContainer)
|
if (!mergedToContainer)
|
||||||
{
|
{
|
||||||
using FileStream fileStream = File.OpenRead(containerPath);
|
using FileStream fileStream = File.OpenRead(containerPath);
|
||||||
using PartitionFileSystem partitionFileSystem = new(fileStream.AsStorage());
|
using PartitionFileSystem partitionFileSystem = new();
|
||||||
|
partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure();
|
||||||
|
|
||||||
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
_virtualFileSystem.ImportTickets(partitionFileSystem);
|
||||||
}
|
}
|
||||||
@@ -259,17 +260,17 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
{
|
{
|
||||||
var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read);
|
var file = new FileStream(aoc.ContainerPath, FileMode.Open, FileAccess.Read);
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
PartitionFileSystem pfs;
|
|
||||||
|
|
||||||
switch (Path.GetExtension(aoc.ContainerPath))
|
switch (Path.GetExtension(aoc.ContainerPath))
|
||||||
{
|
{
|
||||||
case ".xci":
|
case ".xci":
|
||||||
pfs = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
var xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
||||||
pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read);
|
xci.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
break;
|
break;
|
||||||
case ".nsp":
|
case ".nsp":
|
||||||
pfs = new PartitionFileSystem(file.AsStorage());
|
var pfs = new PartitionFileSystem();
|
||||||
pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read);
|
pfs.Initialize(file.AsStorage());
|
||||||
|
pfs.OpenFile(ref ncaFile.Ref, aoc.NcaPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return false; // Print error?
|
return false; // Print error?
|
||||||
@@ -606,11 +607,11 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
|
|
||||||
if (filesystem.FileExists($"{path}/00"))
|
if (filesystem.FileExists($"{path}/00"))
|
||||||
{
|
{
|
||||||
filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode);
|
filesystem.OpenFile(ref file.Ref, $"{path}/00".ToU8Span(), mode).ThrowIfFailure();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode);
|
filesystem.OpenFile(ref file.Ref, path.ToU8Span(), mode).ThrowIfFailure();
|
||||||
}
|
}
|
||||||
|
|
||||||
return file.Release();
|
return file.Release();
|
||||||
|
@@ -7,6 +7,7 @@ using LibHac.Fs.Shim;
|
|||||||
using LibHac.FsSrv;
|
using LibHac.FsSrv;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
using LibHac.Ncm;
|
using LibHac.Ncm;
|
||||||
|
using LibHac.Sdmmc;
|
||||||
using LibHac.Spl;
|
using LibHac.Spl;
|
||||||
using LibHac.Tools.Es;
|
using LibHac.Tools.Es;
|
||||||
using LibHac.Tools.Fs;
|
using LibHac.Tools.Fs;
|
||||||
@@ -32,7 +33,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
|
|
||||||
public KeySet KeySet { get; private set; }
|
public KeySet KeySet { get; private set; }
|
||||||
public EmulatedGameCard GameCard { get; private set; }
|
public EmulatedGameCard GameCard { get; private set; }
|
||||||
public EmulatedSdCard SdCard { get; private set; }
|
public SdmmcApi SdCard { get; private set; }
|
||||||
public ModLoader ModLoader { get; private set; }
|
public ModLoader ModLoader { get; private set; }
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ulong, Stream> _romFsByPid;
|
private readonly ConcurrentDictionary<ulong, Stream> _romFsByPid;
|
||||||
@@ -198,15 +199,15 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator();
|
fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator();
|
||||||
|
|
||||||
GameCard = fsServerObjects.GameCard;
|
GameCard = fsServerObjects.GameCard;
|
||||||
SdCard = fsServerObjects.SdCard;
|
SdCard = fsServerObjects.Sdmmc;
|
||||||
|
|
||||||
SdCard.SetSdCardInsertionStatus(true);
|
SdCard.SetSdCardInserted(true);
|
||||||
|
|
||||||
var fsServerConfig = new FileSystemServerConfig
|
var fsServerConfig = new FileSystemServerConfig
|
||||||
{
|
{
|
||||||
DeviceOperator = fsServerObjects.DeviceOperator,
|
|
||||||
ExternalKeySet = KeySet.ExternalKeySet,
|
ExternalKeySet = KeySet.ExternalKeySet,
|
||||||
FsCreators = fsServerObjects.FsCreators,
|
FsCreators = fsServerObjects.FsCreators,
|
||||||
|
StorageDeviceManagerFactory = fsServerObjects.StorageDeviceManagerFactory,
|
||||||
RandomGenerator = randomGenerator,
|
RandomGenerator = randomGenerator,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -263,7 +264,16 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
|
|
||||||
if (result.IsSuccess())
|
if (result.IsSuccess())
|
||||||
{
|
{
|
||||||
Ticket ticket = new(ticketFile.Get.AsStream());
|
// When reading a file from a Sha256PartitionFileSystem, you can't start a read in the middle
|
||||||
|
// of the hashed portion (usually the first 0x200 bytes) of the file and end the read after
|
||||||
|
// the end of the hashed portion, so we read the ticket file using a single read.
|
||||||
|
byte[] ticketData = new byte[0x2C0];
|
||||||
|
result = ticketFile.Get.Read(out long bytesRead, 0, ticketData);
|
||||||
|
|
||||||
|
if (result.IsFailure() || bytesRead != ticketData.Length)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Ticket ticket = new(new MemoryStream(ticketData));
|
||||||
var titleKey = ticket.GetTitleKey(KeySet);
|
var titleKey = ticket.GetTitleKey(KeySet);
|
||||||
|
|
||||||
if (titleKey != null)
|
if (titleKey != null)
|
||||||
|
@@ -101,7 +101,7 @@ namespace Ryujinx.HLE
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Control if the guest application should be told that there is a Internet connection available.
|
/// Control if the guest application should be told that there is a Internet connection available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal readonly bool EnableInternetAccess;
|
public bool EnableInternetAccess { internal get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Control LibHac's integrity check level.
|
/// Control LibHac's integrity check level.
|
||||||
|
46
src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs
Normal file
46
src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryPermission.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using Ryujinx.Memory;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Kernel.Memory
|
||||||
|
{
|
||||||
|
[Flags]
|
||||||
|
enum KMemoryPermission : uint
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
UserMask = Read | Write | Execute,
|
||||||
|
Mask = uint.MaxValue,
|
||||||
|
|
||||||
|
Read = 1 << 0,
|
||||||
|
Write = 1 << 1,
|
||||||
|
Execute = 1 << 2,
|
||||||
|
DontCare = 1 << 28,
|
||||||
|
|
||||||
|
ReadAndWrite = Read | Write,
|
||||||
|
ReadAndExecute = Read | Execute,
|
||||||
|
}
|
||||||
|
|
||||||
|
static class KMemoryPermissionExtensions
|
||||||
|
{
|
||||||
|
public static MemoryPermission Convert(this KMemoryPermission permission)
|
||||||
|
{
|
||||||
|
MemoryPermission output = MemoryPermission.None;
|
||||||
|
|
||||||
|
if (permission.HasFlag(KMemoryPermission.Read))
|
||||||
|
{
|
||||||
|
output = MemoryPermission.Read;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission.HasFlag(KMemoryPermission.Write))
|
||||||
|
{
|
||||||
|
output |= MemoryPermission.Write;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission.HasFlag(KMemoryPermission.Execute))
|
||||||
|
{
|
||||||
|
output |= MemoryPermission.Execute;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -203,15 +203,17 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission)
|
protected override Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission)
|
||||||
{
|
{
|
||||||
// TODO.
|
_cpuMemory.Reprotect(address, pagesCount * PageSize, permission.Convert());
|
||||||
|
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission)
|
protected override Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission)
|
||||||
{
|
{
|
||||||
// TODO.
|
// TODO: Flush JIT cache.
|
||||||
return Result.Success;
|
|
||||||
|
return Reprotect(address, pagesCount, permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
@@ -1255,7 +1255,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
|||||||
|
|
||||||
if ((oldPermission & KMemoryPermission.Execute) != 0)
|
if ((oldPermission & KMemoryPermission.Execute) != 0)
|
||||||
{
|
{
|
||||||
result = ReprotectWithAttributes(address, pagesCount, permission);
|
result = ReprotectAndFlush(address, pagesCount, permission);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -3036,13 +3036,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory
|
|||||||
protected abstract Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission);
|
protected abstract Result Reprotect(ulong address, ulong pagesCount, KMemoryPermission permission);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Changes the permissions of a given virtual memory region.
|
/// Changes the permissions of a given virtual memory region, while also flushing the cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">Virtual address of the region to have the permission changes</param>
|
/// <param name="address">Virtual address of the region to have the permission changes</param>
|
||||||
/// <param name="pagesCount">Number of pages to have their permissions changed</param>
|
/// <param name="pagesCount">Number of pages to have their permissions changed</param>
|
||||||
/// <param name="permission">New permission</param>
|
/// <param name="permission">New permission</param>
|
||||||
/// <returns>Result of the permission change operation</returns>
|
/// <returns>Result of the permission change operation</returns>
|
||||||
protected abstract Result ReprotectWithAttributes(ulong address, ulong pagesCount, KMemoryPermission permission);
|
protected abstract Result ReprotectAndFlush(ulong address, ulong pagesCount, KMemoryPermission permission);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Alerts the memory tracking that a given region has been read from or written to.
|
/// Alerts the memory tracking that a given region has been read from or written to.
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Kernel.Memory
|
|
||||||
{
|
|
||||||
[Flags]
|
|
||||||
enum KMemoryPermission : uint
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
UserMask = Read | Write | Execute,
|
|
||||||
Mask = uint.MaxValue,
|
|
||||||
|
|
||||||
Read = 1 << 0,
|
|
||||||
Write = 1 << 1,
|
|
||||||
Execute = 1 << 2,
|
|
||||||
DontCare = 1 << 28,
|
|
||||||
|
|
||||||
ReadAndWrite = Read | Write,
|
|
||||||
ReadAndExecute = Read | Execute,
|
|
||||||
}
|
|
||||||
}
|
|
@@ -533,7 +533,9 @@ namespace Ryujinx.HLE.HOS
|
|||||||
|
|
||||||
Logger.Info?.Print(LogClass.ModLoader, "Using replacement ExeFS partition");
|
Logger.Info?.Print(LogClass.ModLoader, "Using replacement ExeFS partition");
|
||||||
|
|
||||||
exefs = new PartitionFileSystem(mods.ExefsContainers[0].Path.OpenRead().AsStorage());
|
var pfs = new PartitionFileSystem();
|
||||||
|
pfs.Initialize(mods.ExefsContainers[0].Path.OpenRead().AsStorage()).ThrowIfFailure();
|
||||||
|
exefs = pfs;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@@ -26,7 +26,9 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
LocalStorage storage = new(pfsPath, FileAccess.Read, FileMode.Open);
|
LocalStorage storage = new(pfsPath, FileAccess.Read, FileMode.Open);
|
||||||
using SharedRef<LibHac.Fs.Fsa.IFileSystem> nsp = new(new PartitionFileSystem(storage));
|
var pfs = new PartitionFileSystem();
|
||||||
|
using SharedRef<LibHac.Fs.Fsa.IFileSystem> nsp = new(pfs);
|
||||||
|
pfs.Initialize(storage).ThrowIfFailure();
|
||||||
|
|
||||||
ImportTitleKeysFromNsp(nsp.Get, context.Device.System.KeySet);
|
ImportTitleKeysFromNsp(nsp.Get, context.Device.System.KeySet);
|
||||||
|
|
||||||
@@ -90,7 +92,8 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
PartitionFileSystem nsp = new(pfsFile.AsStorage());
|
PartitionFileSystem nsp = new();
|
||||||
|
nsp.Initialize(pfsFile.AsStorage()).ThrowIfFailure();
|
||||||
|
|
||||||
ImportTitleKeysFromNsp(nsp, context.Device.System.KeySet);
|
ImportTitleKeysFromNsp(nsp, context.Device.System.KeySet);
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using LibHac;
|
using LibHac;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
using Path = LibHac.FsSrv.Sf.Path;
|
using Path = LibHac.FsSrv.Sf.Path;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
||||||
@@ -202,6 +203,16 @@ namespace Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy
|
|||||||
return (ResultCode)result.Value;
|
return (ResultCode)result.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[CommandCmif(16)]
|
||||||
|
public ResultCode GetFileSystemAttribute(ServiceCtx context)
|
||||||
|
{
|
||||||
|
Result result = _fileSystem.Get.GetFileSystemAttribute(out FileSystemAttribute attribute);
|
||||||
|
|
||||||
|
context.ResponseData.Write(SpanHelpers.AsReadOnlyByteSpan(in attribute));
|
||||||
|
|
||||||
|
return (ResultCode)result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
if (isDisposing)
|
if (isDisposing)
|
||||||
|
@@ -1380,7 +1380,10 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
|||||||
[CommandCmif(1016)]
|
[CommandCmif(1016)]
|
||||||
public ResultCode FlushAccessLogOnSdCard(ServiceCtx context)
|
public ResultCode FlushAccessLogOnSdCard(ServiceCtx context)
|
||||||
{
|
{
|
||||||
return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value;
|
// Logging the access log to the SD card isn't implemented, meaning this function will be a no-op since
|
||||||
|
// there's nothing to flush. Return success until it's implemented.
|
||||||
|
// return (ResultCode)_baseFileSystemProxy.Get.FlushAccessLogOnSdCard().Value;
|
||||||
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
[CommandCmif(1017)]
|
[CommandCmif(1017)]
|
||||||
|
12
src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
Normal file
12
src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn
|
||||||
|
{
|
||||||
|
static class LdnConst
|
||||||
|
{
|
||||||
|
public const int SsidLengthMax = 0x20;
|
||||||
|
public const int AdvertiseDataSizeMax = 0x180;
|
||||||
|
public const int UserNameBytesMax = 0x20;
|
||||||
|
public const int NodeCountMax = 8;
|
||||||
|
public const int StationCountMax = NodeCountMax - 1;
|
||||||
|
public const int PassphraseLengthMax = 0x40;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
@@ -48,7 +48,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
|||||||
{
|
{
|
||||||
result[i].Reserved = new Array7<byte>();
|
result[i].Reserved = new Array7<byte>();
|
||||||
|
|
||||||
if (i < 8)
|
if (i < LdnConst.NodeCountMax)
|
||||||
{
|
{
|
||||||
result[i].State = array[i].State;
|
result[i].State = array[i].State;
|
||||||
array[i].State = NodeLatestUpdateFlags.None;
|
array[i].State = NodeLatestUpdateFlags.None;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
@@ -30,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
_parent.NetworkClient.NetworkChange -= NetworkChanged;
|
_parent.NetworkClient.NetworkChange -= NetworkChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
|
private void NetworkChanged(object sender, NetworkChangeEventArgs e)
|
||||||
{
|
{
|
||||||
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
|
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
|
||||||
|
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
{
|
{
|
||||||
interface INetworkClient : IDisposable
|
interface INetworkClient : IDisposable
|
||||||
{
|
{
|
||||||
|
bool NeedsRealId { get; }
|
||||||
|
|
||||||
event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
||||||
|
|
||||||
void DisconnectNetwork();
|
void DisconnectNetwork();
|
@@ -8,7 +8,7 @@ using Ryujinx.Cpu;
|
|||||||
using Ryujinx.HLE.HOS.Ipc;
|
using Ryujinx.HLE.HOS.Ipc;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn;
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm;
|
||||||
using Ryujinx.Horizon.Common;
|
using Ryujinx.Horizon.Common;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using System;
|
using System;
|
||||||
@@ -395,7 +395,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1)
|
if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
|
||||||
{
|
{
|
||||||
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
||||||
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||||
@@ -546,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment?
|
context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment?
|
||||||
NetworkConfig networkConfig = context.RequestData.ReadStruct<NetworkConfig>();
|
NetworkConfig networkConfig = context.RequestData.ReadStruct<NetworkConfig>();
|
||||||
|
|
||||||
if (networkConfig.IntentId.LocalCommunicationId == -1)
|
if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
|
||||||
{
|
{
|
||||||
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
||||||
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||||
@@ -555,7 +555,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId);
|
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId);
|
||||||
if (!isLocalCommunicationIdValid)
|
if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
|
||||||
{
|
{
|
||||||
return ResultCode.InvalidObject;
|
return ResultCode.InvalidObject;
|
||||||
}
|
}
|
||||||
@@ -568,13 +568,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel);
|
networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel);
|
||||||
securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
|
securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
|
||||||
|
|
||||||
if (networkConfig.NodeCountMax <= 8)
|
if (networkConfig.NodeCountMax <= LdnConst.NodeCountMax)
|
||||||
{
|
{
|
||||||
if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0)
|
if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0)
|
||||||
{
|
{
|
||||||
if (securityConfig.SecurityMode <= SecurityMode.Retail)
|
if (securityConfig.SecurityMode <= SecurityMode.Retail)
|
||||||
{
|
{
|
||||||
if (securityConfig.Passphrase.Length <= 0x40)
|
if (securityConfig.Passphrase.Length <= LdnConst.PassphraseLengthMax)
|
||||||
{
|
{
|
||||||
if (_state == NetworkState.AccessPoint)
|
if (_state == NetworkState.AccessPoint)
|
||||||
{
|
{
|
||||||
@@ -678,7 +678,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
return _nifmResultCode;
|
return _nifmResultCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bufferSize == 0 || bufferSize > 0x180)
|
if (bufferSize == 0 || bufferSize > LdnConst.AdvertiseDataSizeMax)
|
||||||
{
|
{
|
||||||
return ResultCode.InvalidArgument;
|
return ResultCode.InvalidArgument;
|
||||||
}
|
}
|
||||||
@@ -848,10 +848,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
|
|
||||||
context.Memory.Read(bufferPosition, networkInfoBytes);
|
context.Memory.Read(bufferPosition, networkInfoBytes);
|
||||||
|
|
||||||
networkInfo = MemoryMarshal.Cast<byte, NetworkInfo>(networkInfoBytes)[0];
|
networkInfo = MemoryMarshal.Read<NetworkInfo>(networkInfoBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1)
|
if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
|
||||||
{
|
{
|
||||||
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
||||||
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||||
@@ -860,7 +860,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId);
|
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId);
|
||||||
if (!isLocalCommunicationIdValid)
|
if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
|
||||||
{
|
{
|
||||||
return ResultCode.InvalidObject;
|
return ResultCode.InvalidObject;
|
||||||
}
|
}
|
||||||
@@ -1061,10 +1061,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
|
if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
|
||||||
{
|
{
|
||||||
MultiplayerMode mode = context.Device.Configuration.MultiplayerMode;
|
MultiplayerMode mode = context.Device.Configuration.MultiplayerMode;
|
||||||
|
|
||||||
|
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initializing with multiplayer mode: {mode}");
|
||||||
|
|
||||||
switch (mode)
|
switch (mode)
|
||||||
{
|
{
|
||||||
|
case MultiplayerMode.LdnMitm:
|
||||||
|
NetworkClient = new LdnMitmClient(context.Device.Configuration);
|
||||||
|
break;
|
||||||
case MultiplayerMode.Disabled:
|
case MultiplayerMode.Disabled:
|
||||||
NetworkClient = new DisabledLdnClient();
|
NetworkClient = new LdnDisabledClient();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
{
|
{
|
||||||
class DisabledLdnClient : INetworkClient
|
class LdnDisabledClient : INetworkClient
|
||||||
{
|
{
|
||||||
|
public bool NeedsRealId => true;
|
||||||
|
|
||||||
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
||||||
|
|
||||||
public NetworkError Connect(ConnectRequest request)
|
public NetworkError Connect(ConnectRequest request)
|
@@ -0,0 +1,611 @@
|
|||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Memory;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
|
||||||
|
{
|
||||||
|
internal class LanDiscovery : IDisposable
|
||||||
|
{
|
||||||
|
private const int DefaultPort = 11452;
|
||||||
|
private const ushort CommonChannel = 6;
|
||||||
|
private const byte CommonLinkLevel = 3;
|
||||||
|
private const byte CommonNetworkType = 2;
|
||||||
|
|
||||||
|
private const int FailureTimeout = 4000;
|
||||||
|
|
||||||
|
private readonly LdnMitmClient _parent;
|
||||||
|
private readonly LanProtocol _protocol;
|
||||||
|
private bool _initialized;
|
||||||
|
private readonly Ssid _fakeSsid;
|
||||||
|
private ILdnTcpSocket _tcp;
|
||||||
|
private LdnProxyUdpServer _udp, _udp2;
|
||||||
|
private readonly List<LdnProxyTcpSession> _stations = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
private readonly AutoResetEvent _apConnected = new(false);
|
||||||
|
|
||||||
|
internal readonly IPAddress LocalAddr;
|
||||||
|
internal readonly IPAddress LocalBroadcastAddr;
|
||||||
|
internal NetworkInfo NetworkInfo;
|
||||||
|
|
||||||
|
public bool IsHost => _tcp is LdnProxyTcpServer;
|
||||||
|
|
||||||
|
private readonly Random _random = new();
|
||||||
|
|
||||||
|
// NOTE: Credit to https://stackoverflow.com/a/39338188
|
||||||
|
private static IPAddress GetBroadcastAddress(IPAddress address, IPAddress mask)
|
||||||
|
{
|
||||||
|
uint ipAddress = BitConverter.ToUInt32(address.GetAddressBytes(), 0);
|
||||||
|
uint ipMaskV4 = BitConverter.ToUInt32(mask.GetAddressBytes(), 0);
|
||||||
|
uint broadCastIpAddress = ipAddress | ~ipMaskV4;
|
||||||
|
|
||||||
|
return new IPAddress(BitConverter.GetBytes(broadCastIpAddress));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NetworkInfo GetEmptyNetworkInfo()
|
||||||
|
{
|
||||||
|
NetworkInfo networkInfo = new()
|
||||||
|
{
|
||||||
|
NetworkId = new NetworkId
|
||||||
|
{
|
||||||
|
SessionId = new Array16<byte>(),
|
||||||
|
},
|
||||||
|
Common = new CommonNetworkInfo
|
||||||
|
{
|
||||||
|
MacAddress = new Array6<byte>(),
|
||||||
|
Ssid = new Ssid
|
||||||
|
{
|
||||||
|
Name = new Array33<byte>(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Ldn = new LdnNetworkInfo
|
||||||
|
{
|
||||||
|
NodeCountMax = LdnConst.NodeCountMax,
|
||||||
|
SecurityParameter = new Array16<byte>(),
|
||||||
|
Nodes = new Array8<NodeInfo>(),
|
||||||
|
AdvertiseData = new Array384<byte>(),
|
||||||
|
Reserved4 = new Array140<byte>(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < LdnConst.NodeCountMax; i++)
|
||||||
|
{
|
||||||
|
networkInfo.Ldn.Nodes[i] = new NodeInfo
|
||||||
|
{
|
||||||
|
MacAddress = new Array6<byte>(),
|
||||||
|
UserName = new Array33<byte>(),
|
||||||
|
Reserved2 = new Array16<byte>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LanDiscovery(LdnMitmClient parent, IPAddress ipAddress, IPAddress ipv4Mask)
|
||||||
|
{
|
||||||
|
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initialize LanDiscovery using IP: {ipAddress}");
|
||||||
|
|
||||||
|
_parent = parent;
|
||||||
|
LocalAddr = ipAddress;
|
||||||
|
LocalBroadcastAddr = GetBroadcastAddress(ipAddress, ipv4Mask);
|
||||||
|
|
||||||
|
_fakeSsid = new Ssid
|
||||||
|
{
|
||||||
|
Length = LdnConst.SsidLengthMax,
|
||||||
|
};
|
||||||
|
_random.NextBytes(_fakeSsid.Name.AsSpan()[..32]);
|
||||||
|
|
||||||
|
_protocol = new LanProtocol(this);
|
||||||
|
_protocol.Accept += OnConnect;
|
||||||
|
_protocol.SyncNetwork += OnSyncNetwork;
|
||||||
|
_protocol.DisconnectStation += DisconnectStation;
|
||||||
|
|
||||||
|
NetworkInfo = GetEmptyNetworkInfo();
|
||||||
|
|
||||||
|
ResetStations();
|
||||||
|
|
||||||
|
if (!InitUdp())
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Initialize: InitUdp failed.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void OnSyncNetwork(NetworkInfo info)
|
||||||
|
{
|
||||||
|
bool updated = false;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!NetworkInfo.Equals(info))
|
||||||
|
{
|
||||||
|
NetworkInfo = info;
|
||||||
|
updated = true;
|
||||||
|
|
||||||
|
Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"Host IP: {NetworkHelpers.ConvertUint(info.Ldn.Nodes[0].Ipv4Address)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated)
|
||||||
|
{
|
||||||
|
_parent.InvokeNetworkChange(info, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_apConnected.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void OnConnect(LdnProxyTcpSession station)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
station.NodeId = LocateEmptyNode();
|
||||||
|
|
||||||
|
if (_stations.Count > LdnConst.StationCountMax || station.NodeId == -1)
|
||||||
|
{
|
||||||
|
station.Disconnect();
|
||||||
|
station.Dispose();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stations.Add(station);
|
||||||
|
|
||||||
|
UpdateNodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisconnectStation(LdnProxyTcpSession station)
|
||||||
|
{
|
||||||
|
if (!station.IsDisposed)
|
||||||
|
{
|
||||||
|
if (station.IsConnected)
|
||||||
|
{
|
||||||
|
station.Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
station.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_stations.Remove(station))
|
||||||
|
{
|
||||||
|
NetworkInfo.Ldn.Nodes[station.NodeId] = new NodeInfo()
|
||||||
|
{
|
||||||
|
MacAddress = new Array6<byte>(),
|
||||||
|
UserName = new Array33<byte>(),
|
||||||
|
Reserved2 = new Array16<byte>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
UpdateNodes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetAdvertiseData(byte[] data)
|
||||||
|
{
|
||||||
|
if (data.Length > LdnConst.AdvertiseDataSizeMax)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "AdvertiseData exceeds size limit.");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.CopyTo(NetworkInfo.Ldn.AdvertiseData.AsSpan());
|
||||||
|
NetworkInfo.Ldn.AdvertiseDataSize = (ushort)data.Length;
|
||||||
|
|
||||||
|
// NOTE: Otherwise this results in SessionKeepFailed or MasterDisconnected
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (NetworkInfo.Ldn.Nodes[0].IsConnected == 1)
|
||||||
|
{
|
||||||
|
UpdateNodes(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitNetworkInfo()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
NetworkInfo.Common.MacAddress = GetFakeMac();
|
||||||
|
NetworkInfo.Common.Channel = CommonChannel;
|
||||||
|
NetworkInfo.Common.LinkLevel = CommonLinkLevel;
|
||||||
|
NetworkInfo.Common.NetworkType = CommonNetworkType;
|
||||||
|
NetworkInfo.Common.Ssid = _fakeSsid;
|
||||||
|
|
||||||
|
NetworkInfo.Ldn.Nodes = new Array8<NodeInfo>();
|
||||||
|
|
||||||
|
for (int i = 0; i < LdnConst.NodeCountMax; i++)
|
||||||
|
{
|
||||||
|
NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i;
|
||||||
|
NetworkInfo.Ldn.Nodes[i].IsConnected = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Array6<byte> GetFakeMac(IPAddress address = null)
|
||||||
|
{
|
||||||
|
address ??= LocalAddr;
|
||||||
|
|
||||||
|
byte[] ip = address.GetAddressBytes();
|
||||||
|
|
||||||
|
var macAddress = new Array6<byte>();
|
||||||
|
new byte[] { 0x02, 0x00, ip[0], ip[1], ip[2], ip[3] }.CopyTo(macAddress.AsSpan());
|
||||||
|
|
||||||
|
return macAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool InitTcp(bool listening, IPAddress address = null, int port = DefaultPort)
|
||||||
|
{
|
||||||
|
Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"LanDiscovery InitTcp: IP: {address}, listening: {listening}");
|
||||||
|
|
||||||
|
if (_tcp != null)
|
||||||
|
{
|
||||||
|
_tcp.DisconnectAndStop();
|
||||||
|
_tcp.Dispose();
|
||||||
|
_tcp = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ILdnTcpSocket tcpSocket;
|
||||||
|
|
||||||
|
if (listening)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
address ??= LocalAddr;
|
||||||
|
|
||||||
|
tcpSocket = new LdnProxyTcpServer(_protocol, address, port);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpServer: {ex}");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tcpSocket.Start())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (address == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tcpSocket = new LdnProxyTcpClient(_protocol, address, port);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpClient: {ex}");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_tcp = tcpSocket;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool InitUdp()
|
||||||
|
{
|
||||||
|
_udp?.Stop();
|
||||||
|
_udp2?.Stop();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// NOTE: Linux won't receive any broadcast packets if the socket is not bound to the broadcast address.
|
||||||
|
// Windows only works if bound to localhost or the local address.
|
||||||
|
// See this discussion: https://stackoverflow.com/questions/13666789/receiving-udp-broadcast-packets-on-linux
|
||||||
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
_udp2 = new LdnProxyUdpServer(_protocol, LocalBroadcastAddr, DefaultPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
_udp = new LdnProxyUdpServer(_protocol, LocalAddr, DefaultPort);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyUdpServer: {ex}");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetworkInfo[] Scan(ushort channel, ScanFilter filter)
|
||||||
|
{
|
||||||
|
_udp.ClearScanResults();
|
||||||
|
|
||||||
|
if (_protocol.SendBroadcast(_udp, LanPacketType.Scan, DefaultPort) < 0)
|
||||||
|
{
|
||||||
|
return Array.Empty<NetworkInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<NetworkInfo> outNetworkInfo = new();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<ulong, NetworkInfo> item in _udp.GetScanResults())
|
||||||
|
{
|
||||||
|
bool copy = true;
|
||||||
|
|
||||||
|
if (filter.Flag.HasFlag(ScanFilterFlag.LocalCommunicationId))
|
||||||
|
{
|
||||||
|
copy &= filter.NetworkId.IntentId.LocalCommunicationId == item.Value.NetworkId.IntentId.LocalCommunicationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Flag.HasFlag(ScanFilterFlag.SessionId))
|
||||||
|
{
|
||||||
|
copy &= filter.NetworkId.SessionId.AsSpan().SequenceEqual(item.Value.NetworkId.SessionId.AsSpan());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Flag.HasFlag(ScanFilterFlag.NetworkType))
|
||||||
|
{
|
||||||
|
copy &= filter.NetworkType == (NetworkType)item.Value.Common.NetworkType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Flag.HasFlag(ScanFilterFlag.Ssid))
|
||||||
|
{
|
||||||
|
Span<byte> gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..];
|
||||||
|
Span<byte> scanSsid = filter.Ssid.Name.AsSpan()[filter.Ssid.Length..];
|
||||||
|
copy &= gameSsid.SequenceEqual(scanSsid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.Flag.HasFlag(ScanFilterFlag.SceneId))
|
||||||
|
{
|
||||||
|
copy &= filter.NetworkId.IntentId.SceneId == item.Value.NetworkId.IntentId.SceneId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copy)
|
||||||
|
{
|
||||||
|
if (item.Value.Ldn.Nodes[0].UserName[0] != 0)
|
||||||
|
{
|
||||||
|
outNetworkInfo.Add(item.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Scan: Got empty Username. There might be a timing issue somewhere...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outNetworkInfo.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ResetStations()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
foreach (LdnProxyTcpSession station in _stations)
|
||||||
|
{
|
||||||
|
station.Disconnect();
|
||||||
|
station.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_stations.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int LocateEmptyNode()
|
||||||
|
{
|
||||||
|
Array8<NodeInfo> nodes = NetworkInfo.Ldn.Nodes;
|
||||||
|
|
||||||
|
for (int i = 1; i < nodes.Length; i++)
|
||||||
|
{
|
||||||
|
if (nodes[i].IsConnected == 0)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateNodes(bool forceUpdate = false)
|
||||||
|
{
|
||||||
|
int countConnected = 1;
|
||||||
|
|
||||||
|
foreach (LdnProxyTcpSession station in _stations.Where(station => station.IsConnected))
|
||||||
|
{
|
||||||
|
countConnected++;
|
||||||
|
|
||||||
|
station.OverrideInfo();
|
||||||
|
|
||||||
|
// NOTE: This is not part of the original implementation.
|
||||||
|
NetworkInfo.Ldn.Nodes[station.NodeId] = station.NodeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte nodeCount = (byte)countConnected;
|
||||||
|
|
||||||
|
bool networkInfoChanged = forceUpdate || NetworkInfo.Ldn.NodeCount != nodeCount;
|
||||||
|
|
||||||
|
NetworkInfo.Ldn.NodeCount = nodeCount;
|
||||||
|
|
||||||
|
foreach (LdnProxyTcpSession station in _stations)
|
||||||
|
{
|
||||||
|
if (station.IsConnected)
|
||||||
|
{
|
||||||
|
if (_protocol.SendPacket(station, LanPacketType.SyncNetwork, SpanHelpers.AsSpan<NetworkInfo, byte>(ref NetworkInfo).ToArray()) < 0)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to send {LanPacketType.SyncNetwork} to station {station.NodeId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkInfoChanged)
|
||||||
|
{
|
||||||
|
_parent.InvokeNetworkChange(NetworkInfo, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected NodeInfo GetNodeInfo(NodeInfo node, UserConfig userConfig, ushort localCommunicationVersion)
|
||||||
|
{
|
||||||
|
uint ipAddress = NetworkHelpers.ConvertIpv4Address(LocalAddr);
|
||||||
|
|
||||||
|
node.MacAddress = GetFakeMac();
|
||||||
|
node.IsConnected = 1;
|
||||||
|
node.UserName = userConfig.UserName;
|
||||||
|
node.LocalCommunicationVersion = localCommunicationVersion;
|
||||||
|
node.Ipv4Address = ipAddress;
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig)
|
||||||
|
{
|
||||||
|
if (!InitTcp(true))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
InitNetworkInfo();
|
||||||
|
|
||||||
|
NetworkInfo.Ldn.NodeCountMax = networkConfig.NodeCountMax;
|
||||||
|
NetworkInfo.Ldn.SecurityMode = (ushort)securityConfig.SecurityMode;
|
||||||
|
|
||||||
|
NetworkInfo.Common.Channel = networkConfig.Channel == 0 ? (ushort)6 : networkConfig.Channel;
|
||||||
|
|
||||||
|
NetworkInfo.NetworkId.SessionId = new Array16<byte>();
|
||||||
|
_random.NextBytes(NetworkInfo.NetworkId.SessionId.AsSpan());
|
||||||
|
NetworkInfo.NetworkId.IntentId = networkConfig.IntentId;
|
||||||
|
|
||||||
|
NetworkInfo.Ldn.Nodes[0] = GetNodeInfo(NetworkInfo.Ldn.Nodes[0], userConfig, networkConfig.LocalCommunicationVersion);
|
||||||
|
NetworkInfo.Ldn.Nodes[0].IsConnected = 1;
|
||||||
|
NetworkInfo.Ldn.NodeCount++;
|
||||||
|
|
||||||
|
_parent.InvokeNetworkChange(NetworkInfo, true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DestroyNetwork()
|
||||||
|
{
|
||||||
|
if (_tcp != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_tcp.DisconnectAndStop();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_tcp.Dispose();
|
||||||
|
_tcp = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetStations();
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetworkError Connect(NetworkInfo networkInfo, UserConfig userConfig, uint localCommunicationVersion)
|
||||||
|
{
|
||||||
|
_apConnected.Reset();
|
||||||
|
|
||||||
|
if (networkInfo.Ldn.NodeCount == 0)
|
||||||
|
{
|
||||||
|
return NetworkError.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPAddress address = NetworkHelpers.ConvertUint(networkInfo.Ldn.Nodes[0].Ipv4Address);
|
||||||
|
|
||||||
|
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Connecting to host: {address}");
|
||||||
|
|
||||||
|
if (!InitTcp(false, address))
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Could not initialize TCPClient");
|
||||||
|
|
||||||
|
return NetworkError.ConnectNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_tcp.Connect())
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Failed to connect.");
|
||||||
|
|
||||||
|
return NetworkError.ConnectFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeInfo myNode = GetNodeInfo(new NodeInfo(), userConfig, (ushort)localCommunicationVersion);
|
||||||
|
if (_protocol.SendPacket(_tcp, LanPacketType.Connect, SpanHelpers.AsSpan<NodeInfo, byte>(ref myNode).ToArray()) < 0)
|
||||||
|
{
|
||||||
|
return NetworkError.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _apConnected.WaitOne(FailureTimeout) ? NetworkError.None : NetworkError.ConnectTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
{
|
||||||
|
DisconnectAndStop();
|
||||||
|
ResetStations();
|
||||||
|
_initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_protocol.Accept -= OnConnect;
|
||||||
|
_protocol.SyncNetwork -= OnSyncNetwork;
|
||||||
|
_protocol.DisconnectStation -= DisconnectStation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisconnectAndStop()
|
||||||
|
{
|
||||||
|
if (_udp != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_udp.Stop();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_udp.Dispose();
|
||||||
|
_udp = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_udp2 != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_udp2.Stop();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_udp2.Dispose();
|
||||||
|
_udp2 = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_tcp != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_tcp.DisconnectAndStop();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_tcp.Dispose();
|
||||||
|
_tcp = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,314 @@
|
|||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Memory;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
|
||||||
|
{
|
||||||
|
internal class LanProtocol
|
||||||
|
{
|
||||||
|
private const uint LanMagic = 0x11451400;
|
||||||
|
|
||||||
|
public const int BufferSize = 2048;
|
||||||
|
public const int TcpTxBufferSize = 0x800;
|
||||||
|
public const int TcpRxBufferSize = 0x1000;
|
||||||
|
public const int TxBufferSizeMax = 0x2000;
|
||||||
|
public const int RxBufferSizeMax = 0x2000;
|
||||||
|
|
||||||
|
private readonly int _headerSize = Marshal.SizeOf<LanPacketHeader>();
|
||||||
|
|
||||||
|
private readonly LanDiscovery _discovery;
|
||||||
|
|
||||||
|
public event Action<LdnProxyTcpSession> Accept;
|
||||||
|
public event Action<EndPoint, LanPacketType, byte[]> Scan;
|
||||||
|
public event Action<NetworkInfo> ScanResponse;
|
||||||
|
public event Action<NetworkInfo> SyncNetwork;
|
||||||
|
public event Action<NodeInfo, EndPoint> Connect;
|
||||||
|
public event Action<LdnProxyTcpSession> DisconnectStation;
|
||||||
|
|
||||||
|
public LanProtocol(LanDiscovery parent)
|
||||||
|
{
|
||||||
|
_discovery = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeAccept(LdnProxyTcpSession session)
|
||||||
|
{
|
||||||
|
Accept?.Invoke(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeDisconnectStation(LdnProxyTcpSession session)
|
||||||
|
{
|
||||||
|
DisconnectStation?.Invoke(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DecodeAndHandle(LanPacketHeader header, byte[] data, EndPoint endPoint = null)
|
||||||
|
{
|
||||||
|
switch (header.Type)
|
||||||
|
{
|
||||||
|
case LanPacketType.Scan:
|
||||||
|
// UDP
|
||||||
|
if (_discovery.IsHost)
|
||||||
|
{
|
||||||
|
Scan?.Invoke(endPoint, LanPacketType.ScanResponse, SpanHelpers.AsSpan<NetworkInfo, byte>(ref _discovery.NetworkInfo).ToArray());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case LanPacketType.ScanResponse:
|
||||||
|
// UDP
|
||||||
|
ScanResponse?.Invoke(MemoryMarshal.Cast<byte, NetworkInfo>(data)[0]);
|
||||||
|
break;
|
||||||
|
case LanPacketType.SyncNetwork:
|
||||||
|
// TCP
|
||||||
|
SyncNetwork?.Invoke(MemoryMarshal.Cast<byte, NetworkInfo>(data)[0]);
|
||||||
|
break;
|
||||||
|
case LanPacketType.Connect:
|
||||||
|
// TCP Session / Station
|
||||||
|
Connect?.Invoke(MemoryMarshal.Cast<byte, NodeInfo>(data)[0], endPoint);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decode error: Unhandled type {header.Type}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Read(scoped ref byte[] buffer, scoped ref int bufferEnd, byte[] data, int offset, int size, EndPoint endPoint = null)
|
||||||
|
{
|
||||||
|
if (endPoint != null && _discovery.LocalAddr.Equals(((IPEndPoint)endPoint).Address))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while (index < size)
|
||||||
|
{
|
||||||
|
if (bufferEnd < _headerSize)
|
||||||
|
{
|
||||||
|
int copyable2 = Math.Min(size - index, Math.Min(size, _headerSize - bufferEnd));
|
||||||
|
|
||||||
|
Array.Copy(data, index + offset, buffer, bufferEnd, copyable2);
|
||||||
|
|
||||||
|
index += copyable2;
|
||||||
|
bufferEnd += copyable2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferEnd >= _headerSize)
|
||||||
|
{
|
||||||
|
LanPacketHeader header = MemoryMarshal.Cast<byte, LanPacketHeader>(buffer)[0];
|
||||||
|
if (header.Magic != LanMagic)
|
||||||
|
{
|
||||||
|
bufferEnd = 0;
|
||||||
|
|
||||||
|
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, $"Invalid magic number in received packet. [magic: {header.Magic}] [EP: {endPoint}]");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalSize = _headerSize + header.Length;
|
||||||
|
if (totalSize > BufferSize)
|
||||||
|
{
|
||||||
|
bufferEnd = 0;
|
||||||
|
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Max packet size {BufferSize} exceeded.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int copyable = Math.Min(size - index, Math.Min(size, totalSize - bufferEnd));
|
||||||
|
|
||||||
|
Array.Copy(data, index + offset, buffer, bufferEnd, copyable);
|
||||||
|
|
||||||
|
index += copyable;
|
||||||
|
bufferEnd += copyable;
|
||||||
|
|
||||||
|
if (totalSize == bufferEnd)
|
||||||
|
{
|
||||||
|
byte[] ldnData = new byte[totalSize - _headerSize];
|
||||||
|
Array.Copy(buffer, _headerSize, ldnData, 0, ldnData.Length);
|
||||||
|
|
||||||
|
if (header.Compressed == 1)
|
||||||
|
{
|
||||||
|
if (Decompress(ldnData, out byte[] decompressedLdnData) != 0)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error:\n {header}, {_headerSize}\n {ldnData}, {ldnData.Length}");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decompressedLdnData.Length != header.DecompressLength)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error: length does not match. ({decompressedLdnData.Length} != {header.DecompressLength})");
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error data: '{string.Join("", decompressedLdnData.Select(x => (int)x).ToArray())}'");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ldnData = decompressedLdnData;
|
||||||
|
}
|
||||||
|
|
||||||
|
DecodeAndHandle(header, ldnData, endPoint);
|
||||||
|
|
||||||
|
bufferEnd = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SendBroadcast(ILdnSocket s, LanPacketType type, int port)
|
||||||
|
{
|
||||||
|
return SendPacket(s, type, Array.Empty<byte>(), new IPEndPoint(_discovery.LocalBroadcastAddr, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SendPacket(ILdnSocket s, LanPacketType type, byte[] data, EndPoint endPoint = null)
|
||||||
|
{
|
||||||
|
byte[] buf = PreparePacket(type, data);
|
||||||
|
|
||||||
|
return s.SendPacketAsync(endPoint, buf) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SendPacket(LdnProxyTcpSession s, LanPacketType type, byte[] data)
|
||||||
|
{
|
||||||
|
byte[] buf = PreparePacket(type, data);
|
||||||
|
|
||||||
|
return s.SendAsync(buf) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LanPacketHeader PrepareHeader(LanPacketHeader header, LanPacketType type)
|
||||||
|
{
|
||||||
|
header.Magic = LanMagic;
|
||||||
|
header.Type = type;
|
||||||
|
header.Compressed = 0;
|
||||||
|
header.Length = 0;
|
||||||
|
header.DecompressLength = 0;
|
||||||
|
header.Reserved = new Array2<byte>();
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] PreparePacket(LanPacketType type, byte[] data)
|
||||||
|
{
|
||||||
|
LanPacketHeader header = PrepareHeader(new LanPacketHeader(), type);
|
||||||
|
header.Length = (ushort)data.Length;
|
||||||
|
|
||||||
|
byte[] buf;
|
||||||
|
if (data.Length > 0)
|
||||||
|
{
|
||||||
|
if (Compress(data, out byte[] compressed) == 0)
|
||||||
|
{
|
||||||
|
header.DecompressLength = header.Length;
|
||||||
|
header.Length = (ushort)compressed.Length;
|
||||||
|
header.Compressed = 1;
|
||||||
|
|
||||||
|
buf = new byte[compressed.Length + _headerSize];
|
||||||
|
|
||||||
|
SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
|
||||||
|
compressed.CopyTo(buf, _headerSize);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buf = new byte[data.Length + _headerSize];
|
||||||
|
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Compressing packet data failed.");
|
||||||
|
|
||||||
|
SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
|
||||||
|
data.CopyTo(buf, _headerSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buf = new byte[_headerSize];
|
||||||
|
SpanHelpers.AsSpan<LanPacketHeader, byte>(ref header).ToArray().CopyTo(buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Compress(byte[] input, out byte[] output)
|
||||||
|
{
|
||||||
|
List<byte> outputList = new();
|
||||||
|
int i = 0;
|
||||||
|
int maxCount = 0xFF;
|
||||||
|
|
||||||
|
while (i < input.Length)
|
||||||
|
{
|
||||||
|
byte inputByte = input[i++];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
if (inputByte == 0)
|
||||||
|
{
|
||||||
|
while (i < input.Length && input[i] == 0 && count < maxCount)
|
||||||
|
{
|
||||||
|
count += 1;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputByte == 0)
|
||||||
|
{
|
||||||
|
outputList.Add(0);
|
||||||
|
|
||||||
|
if (outputList.Count == BufferSize)
|
||||||
|
{
|
||||||
|
output = null;
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputList.Add((byte)count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
outputList.Add(inputByte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output = outputList.ToArray();
|
||||||
|
|
||||||
|
return i == input.Length ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Decompress(byte[] input, out byte[] output)
|
||||||
|
{
|
||||||
|
List<byte> outputList = new();
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
while (i < input.Length && outputList.Count < BufferSize)
|
||||||
|
{
|
||||||
|
byte inputByte = input[i++];
|
||||||
|
|
||||||
|
outputList.Add(inputByte);
|
||||||
|
|
||||||
|
if (inputByte == 0)
|
||||||
|
{
|
||||||
|
if (i == input.Length)
|
||||||
|
{
|
||||||
|
output = null;
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = input[i++];
|
||||||
|
|
||||||
|
for (int j = 0; j < count; j++)
|
||||||
|
{
|
||||||
|
if (outputList.Count == BufferSize)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputList.Add(inputByte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output = outputList.ToArray();
|
||||||
|
|
||||||
|
return i == input.Length ? 0 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,104 @@
|
|||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||||
|
using System;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client implementation for <a href="https://github.com/spacemeowx2/ldn_mitm">ldn_mitm</a>
|
||||||
|
/// </summary>
|
||||||
|
internal class LdnMitmClient : INetworkClient
|
||||||
|
{
|
||||||
|
public bool NeedsRealId => false;
|
||||||
|
|
||||||
|
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
||||||
|
|
||||||
|
private readonly LanDiscovery _lanDiscovery;
|
||||||
|
|
||||||
|
public LdnMitmClient(HLEConfiguration config)
|
||||||
|
{
|
||||||
|
UnicastIPAddressInformation localIpInterface = NetworkHelpers.GetLocalInterface(config.MultiplayerLanInterfaceId).Item2;
|
||||||
|
|
||||||
|
_lanDiscovery = new LanDiscovery(this, localIpInterface.Address, localIpInterface.IPv4Mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void InvokeNetworkChange(NetworkInfo info, bool connected, DisconnectReason reason = DisconnectReason.None)
|
||||||
|
{
|
||||||
|
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, connected: connected, disconnectReason: reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetworkError Connect(ConnectRequest request)
|
||||||
|
{
|
||||||
|
return _lanDiscovery.Connect(request.NetworkInfo, request.UserConfig, request.LocalCommunicationVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetworkError ConnectPrivate(ConnectPrivateRequest request)
|
||||||
|
{
|
||||||
|
// NOTE: This method is not implemented in ldn_mitm
|
||||||
|
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient ConnectPrivate");
|
||||||
|
|
||||||
|
return NetworkError.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
|
||||||
|
{
|
||||||
|
return _lanDiscovery.CreateNetwork(request.SecurityConfig, request.UserConfig, request.NetworkConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
|
||||||
|
{
|
||||||
|
// NOTE: This method is not implemented in ldn_mitm
|
||||||
|
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient CreateNetworkPrivate");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisconnectAndStop()
|
||||||
|
{
|
||||||
|
_lanDiscovery.DisconnectAndStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisconnectNetwork()
|
||||||
|
{
|
||||||
|
_lanDiscovery.DestroyNetwork();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
|
||||||
|
{
|
||||||
|
// NOTE: This method is not implemented in ldn_mitm
|
||||||
|
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient Reject");
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
|
||||||
|
{
|
||||||
|
return _lanDiscovery.Scan(channel, scanFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetAdvertiseData(byte[] data)
|
||||||
|
{
|
||||||
|
_lanDiscovery.SetAdvertiseData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetGameVersion(byte[] versionString)
|
||||||
|
{
|
||||||
|
// NOTE: This method is not implemented in ldn_mitm
|
||||||
|
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetGameVersion");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
|
||||||
|
{
|
||||||
|
// NOTE: This method is not implemented in ldn_mitm
|
||||||
|
Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetStationAcceptPolicy");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_lanDiscovery.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
|
||||||
|
{
|
||||||
|
internal interface ILdnSocket : IDisposable
|
||||||
|
{
|
||||||
|
bool SendPacketAsync(EndPoint endpoint, byte[] buffer);
|
||||||
|
bool Start();
|
||||||
|
bool Stop();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
|
||||||
|
{
|
||||||
|
internal interface ILdnTcpSocket : ILdnSocket
|
||||||
|
{
|
||||||
|
bool Connect();
|
||||||
|
void DisconnectAndStop();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,99 @@
|
|||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
|
||||||
|
{
|
||||||
|
internal class LdnProxyTcpClient : NetCoreServer.TcpClient, ILdnTcpSocket
|
||||||
|
{
|
||||||
|
private readonly LanProtocol _protocol;
|
||||||
|
private byte[] _buffer;
|
||||||
|
private int _bufferEnd;
|
||||||
|
|
||||||
|
public LdnProxyTcpClient(LanProtocol protocol, IPAddress address, int port) : base(address, port)
|
||||||
|
{
|
||||||
|
_protocol = protocol;
|
||||||
|
_buffer = new byte[LanProtocol.BufferSize];
|
||||||
|
OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
|
||||||
|
OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
|
||||||
|
OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
|
||||||
|
OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnConnected()
|
||||||
|
{
|
||||||
|
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient connected!");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnReceived(byte[] buffer, long offset, long size)
|
||||||
|
{
|
||||||
|
_protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisconnectAndStop()
|
||||||
|
{
|
||||||
|
DisconnectAsync();
|
||||||
|
|
||||||
|
while (IsConnected)
|
||||||
|
{
|
||||||
|
Thread.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SendPacketAsync(EndPoint endPoint, byte[] data)
|
||||||
|
{
|
||||||
|
if (endPoint != null)
|
||||||
|
{
|
||||||
|
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTcpClient is sending a packet but endpoint is not null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsConnecting && !IsConnected)
|
||||||
|
{
|
||||||
|
Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPClient needs to connect before sending packets. Waiting...");
|
||||||
|
|
||||||
|
while (IsConnecting && !IsConnected)
|
||||||
|
{
|
||||||
|
Thread.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendAsync(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnError(SocketError error)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient caught an error with code {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposingManagedResources)
|
||||||
|
{
|
||||||
|
DisconnectAndStop();
|
||||||
|
base.Dispose(disposingManagedResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Connect()
|
||||||
|
{
|
||||||
|
// TODO: NetCoreServer has a Connect() method, but it currently leads to weird issues.
|
||||||
|
base.ConnectAsync();
|
||||||
|
|
||||||
|
while (IsConnecting)
|
||||||
|
{
|
||||||
|
Thread.Sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Start()
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Start was called.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Stop()
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Stop was called.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
using NetCoreServer;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
|
||||||
|
{
|
||||||
|
internal class LdnProxyTcpServer : TcpServer, ILdnTcpSocket
|
||||||
|
{
|
||||||
|
private readonly LanProtocol _protocol;
|
||||||
|
|
||||||
|
public LdnProxyTcpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
|
||||||
|
{
|
||||||
|
_protocol = protocol;
|
||||||
|
OptionReuseAddress = true;
|
||||||
|
OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
|
||||||
|
OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
|
||||||
|
|
||||||
|
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer created a server for this address: {address}:{port}");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TcpSession CreateSession()
|
||||||
|
{
|
||||||
|
return new LdnProxyTcpSession(this, _protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnError(SocketError error)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer caught an error with code {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposingManagedResources)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
base.Dispose(disposingManagedResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Connect()
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Connect was called.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisconnectAndStop()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SendPacketAsync(EndPoint endpoint, byte[] buffer)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("SendPacketAsync was called.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
|
||||||
|
{
|
||||||
|
internal class LdnProxyTcpSession : NetCoreServer.TcpSession
|
||||||
|
{
|
||||||
|
private readonly LanProtocol _protocol;
|
||||||
|
|
||||||
|
internal int NodeId;
|
||||||
|
internal NodeInfo NodeInfo;
|
||||||
|
|
||||||
|
private byte[] _buffer;
|
||||||
|
private int _bufferEnd;
|
||||||
|
|
||||||
|
public LdnProxyTcpSession(LdnProxyTcpServer server, LanProtocol protocol) : base(server)
|
||||||
|
{
|
||||||
|
_protocol = protocol;
|
||||||
|
_protocol.Connect += OnConnect;
|
||||||
|
_buffer = new byte[LanProtocol.BufferSize];
|
||||||
|
OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
|
||||||
|
OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
|
||||||
|
OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
|
||||||
|
OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OverrideInfo()
|
||||||
|
{
|
||||||
|
NodeInfo.NodeId = (byte)NodeId;
|
||||||
|
NodeInfo.IsConnected = (byte)(IsConnected ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnConnected()
|
||||||
|
{
|
||||||
|
Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession connected!");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisconnected()
|
||||||
|
{
|
||||||
|
Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession disconnected!");
|
||||||
|
|
||||||
|
_protocol.InvokeDisconnectStation(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnReceived(byte[] buffer, long offset, long size)
|
||||||
|
{
|
||||||
|
_protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, this.Socket.RemoteEndPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnError(SocketError error)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession caught an error with code {error}");
|
||||||
|
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposingManagedResources)
|
||||||
|
{
|
||||||
|
_protocol.Connect -= OnConnect;
|
||||||
|
base.Dispose(disposingManagedResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConnect(NodeInfo info, EndPoint endPoint)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (endPoint.Equals(this.Socket.RemoteEndPoint))
|
||||||
|
{
|
||||||
|
NodeInfo = info;
|
||||||
|
_protocol.InvokeAccept(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.ObjectDisposedException)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession was disposed. [IP: {NodeInfo.Ipv4Address}]");
|
||||||
|
|
||||||
|
_protocol.InvokeDisconnectStation(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,157 @@
|
|||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
|
||||||
|
{
|
||||||
|
internal class LdnProxyUdpServer : NetCoreServer.UdpServer, ILdnSocket
|
||||||
|
{
|
||||||
|
private const long ScanFrequency = 1000;
|
||||||
|
|
||||||
|
private readonly LanProtocol _protocol;
|
||||||
|
private byte[] _buffer;
|
||||||
|
private int _bufferEnd;
|
||||||
|
|
||||||
|
private readonly object _scanLock = new();
|
||||||
|
|
||||||
|
private Dictionary<ulong, NetworkInfo> _scanResultsLast = new();
|
||||||
|
private Dictionary<ulong, NetworkInfo> _scanResults = new();
|
||||||
|
private readonly AutoResetEvent _scanResponse = new(false);
|
||||||
|
private long _lastScanTime;
|
||||||
|
|
||||||
|
public LdnProxyUdpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
|
||||||
|
{
|
||||||
|
_protocol = protocol;
|
||||||
|
_protocol.Scan += HandleScan;
|
||||||
|
_protocol.ScanResponse += HandleScanResponse;
|
||||||
|
_buffer = new byte[LanProtocol.BufferSize];
|
||||||
|
OptionReuseAddress = true;
|
||||||
|
OptionReceiveBufferSize = LanProtocol.RxBufferSizeMax;
|
||||||
|
OptionSendBufferSize = LanProtocol.TxBufferSizeMax;
|
||||||
|
|
||||||
|
Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Socket CreateSocket()
|
||||||
|
{
|
||||||
|
return new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp)
|
||||||
|
{
|
||||||
|
EnableBroadcast = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStarted()
|
||||||
|
{
|
||||||
|
ReceiveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size)
|
||||||
|
{
|
||||||
|
_protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, endpoint);
|
||||||
|
ReceiveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnError(SocketError error)
|
||||||
|
{
|
||||||
|
Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyUdpServer caught an error with code {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposingManagedResources)
|
||||||
|
{
|
||||||
|
_protocol.Scan -= HandleScan;
|
||||||
|
_protocol.ScanResponse -= HandleScanResponse;
|
||||||
|
|
||||||
|
_scanResponse.Dispose();
|
||||||
|
|
||||||
|
base.Dispose(disposingManagedResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SendPacketAsync(EndPoint endpoint, byte[] data)
|
||||||
|
{
|
||||||
|
return SendAsync(endpoint, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleScan(EndPoint endpoint, LanPacketType type, byte[] data)
|
||||||
|
{
|
||||||
|
_protocol.SendPacket(this, type, data, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleScanResponse(NetworkInfo info)
|
||||||
|
{
|
||||||
|
Span<byte> mac = stackalloc byte[8];
|
||||||
|
|
||||||
|
info.Common.MacAddress.AsSpan().CopyTo(mac);
|
||||||
|
|
||||||
|
lock (_scanLock)
|
||||||
|
{
|
||||||
|
_scanResults[BitConverter.ToUInt64(mac)] = info;
|
||||||
|
|
||||||
|
_scanResponse.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearScanResults()
|
||||||
|
{
|
||||||
|
// Rate limit scans.
|
||||||
|
|
||||||
|
long timeMs = Stopwatch.GetTimestamp() / (Stopwatch.Frequency / 1000);
|
||||||
|
long delay = ScanFrequency - (timeMs - _lastScanTime);
|
||||||
|
|
||||||
|
if (delay > 0)
|
||||||
|
{
|
||||||
|
Thread.Sleep((int)delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastScanTime = timeMs;
|
||||||
|
|
||||||
|
lock (_scanLock)
|
||||||
|
{
|
||||||
|
var newResults = _scanResultsLast;
|
||||||
|
newResults.Clear();
|
||||||
|
|
||||||
|
_scanResultsLast = _scanResults;
|
||||||
|
_scanResults = newResults;
|
||||||
|
|
||||||
|
_scanResponse.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<ulong, NetworkInfo> GetScanResults()
|
||||||
|
{
|
||||||
|
// NOTE: Try to minimize waiting time for scan results.
|
||||||
|
// After we receive the first response, wait a short time for follow-ups and return.
|
||||||
|
// Responses that were too late to catch will appear in the next scan.
|
||||||
|
|
||||||
|
// ldn_mitm does not do this, but this improves latency for games that expect it to be low (it is on console).
|
||||||
|
|
||||||
|
if (_scanResponse.WaitOne(1000))
|
||||||
|
{
|
||||||
|
// Wait a short while longer in case there are some other responses.
|
||||||
|
Thread.Sleep(33);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_scanLock)
|
||||||
|
{
|
||||||
|
var results = new Dictionary<ulong, NetworkInfo>();
|
||||||
|
|
||||||
|
foreach (KeyValuePair<ulong, NetworkInfo> last in _scanResultsLast)
|
||||||
|
{
|
||||||
|
results[last.Key] = last.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (KeyValuePair<ulong, NetworkInfo> scan in _scanResults)
|
||||||
|
{
|
||||||
|
results[scan.Key] = scan.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
using Ryujinx.Common.Memory;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
|
||||||
|
{
|
||||||
|
[StructLayout(LayoutKind.Sequential, Size = 12)]
|
||||||
|
internal struct LanPacketHeader
|
||||||
|
{
|
||||||
|
public uint Magic;
|
||||||
|
public LanPacketType Type;
|
||||||
|
public byte Compressed;
|
||||||
|
public ushort Length;
|
||||||
|
public ushort DecompressLength;
|
||||||
|
public Array2<byte> Reserved;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
|
||||||
|
{
|
||||||
|
internal enum LanPacketType : byte
|
||||||
|
{
|
||||||
|
Scan,
|
||||||
|
ScanResponse,
|
||||||
|
Connect,
|
||||||
|
SyncNetwork,
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
{
|
{
|
||||||
class NetworkChangeEventArgs : EventArgs
|
class NetworkChangeEventArgs : EventArgs
|
||||||
{
|
{
|
@@ -1,7 +1,6 @@
|
|||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
@@ -22,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
|||||||
_parent.NetworkClient.NetworkChange += NetworkChanged;
|
_parent.NetworkClient.NetworkChange += NetworkChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
|
private void NetworkChanged(object sender, NetworkChangeEventArgs e)
|
||||||
{
|
{
|
||||||
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
|
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 0xBC)]
|
[StructLayout(LayoutKind.Sequential, Size = 0xBC)]
|
||||||
struct ConnectPrivateRequest
|
struct ConnectPrivateRequest
|
@@ -1,7 +1,7 @@
|
|||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 0x4FC)]
|
[StructLayout(LayoutKind.Sequential, Size = 0x4FC)]
|
||||||
struct ConnectRequest
|
struct ConnectRequest
|
@@ -1,7 +1,7 @@
|
|||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
{
|
{
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Advertise data is appended separately (remaining data in the buffer).
|
/// Advertise data is appended separately (remaining data in the buffer).
|
@@ -1,7 +1,7 @@
|
|||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
{
|
{
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Advertise data is appended separately (remaining data in the buffer).
|
/// Advertise data is appended separately (remaining data in the buffer).
|
@@ -1,4 +1,4 @@
|
|||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
{
|
{
|
||||||
enum NetworkError : int
|
enum NetworkError : int
|
||||||
{
|
{
|
@@ -1,6 +1,6 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 0x4)]
|
[StructLayout(LayoutKind.Sequential, Size = 0x4)]
|
||||||
struct NetworkErrorMessage
|
struct NetworkErrorMessage
|
@@ -39,6 +39,8 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
private readonly KernelContext _context;
|
private readonly KernelContext _context;
|
||||||
private KProcess _selfProcess;
|
private KProcess _selfProcess;
|
||||||
private KThread _selfThread;
|
private KThread _selfThread;
|
||||||
|
private KEvent _wakeEvent;
|
||||||
|
private int _wakeHandle = 0;
|
||||||
|
|
||||||
private readonly ReaderWriterLockSlim _handleLock = new();
|
private readonly ReaderWriterLockSlim _handleLock = new();
|
||||||
private readonly Dictionary<int, IpcService> _sessions = new();
|
private readonly Dictionary<int, IpcService> _sessions = new();
|
||||||
@@ -125,6 +127,8 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
_handleLock.ExitWriteLock();
|
_handleLock.ExitWriteLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_wakeEvent.WritableEvent.Signal();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IpcService GetSessionObj(int serverSessionHandle)
|
private IpcService GetSessionObj(int serverSessionHandle)
|
||||||
@@ -187,6 +191,9 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
AddPort(serverPortHandle, SmObjectFactory);
|
AddPort(serverPortHandle, SmObjectFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_wakeEvent = new KEvent(_context);
|
||||||
|
Result result = _selfProcess.HandleTable.GenerateHandle(_wakeEvent.ReadableEvent, out _wakeHandle);
|
||||||
|
|
||||||
InitDone.Set();
|
InitDone.Set();
|
||||||
|
|
||||||
ulong messagePtr = _selfThread.TlsAddress;
|
ulong messagePtr = _selfThread.TlsAddress;
|
||||||
@@ -195,7 +202,6 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
_selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
|
_selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
|
||||||
_selfProcess.CpuMemory.Write(messagePtr + 0x4, 2 << 10);
|
_selfProcess.CpuMemory.Write(messagePtr + 0x4, 2 << 10);
|
||||||
_selfProcess.CpuMemory.Write(messagePtr + 0x8, heapAddr | ((ulong)PointerBufferSize << 48));
|
_selfProcess.CpuMemory.Write(messagePtr + 0x8, heapAddr | ((ulong)PointerBufferSize << 48));
|
||||||
|
|
||||||
int replyTargetHandle = 0;
|
int replyTargetHandle = 0;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
@@ -211,13 +217,15 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
|
|
||||||
portHandleCount = _ports.Count;
|
portHandleCount = _ports.Count;
|
||||||
|
|
||||||
handleCount = portHandleCount + _sessions.Count;
|
handleCount = portHandleCount + _sessions.Count + 1;
|
||||||
|
|
||||||
handles = ArrayPool<int>.Shared.Rent(handleCount);
|
handles = ArrayPool<int>.Shared.Rent(handleCount);
|
||||||
|
|
||||||
_ports.Keys.CopyTo(handles, 0);
|
handles[0] = _wakeHandle;
|
||||||
|
|
||||||
_sessions.Keys.CopyTo(handles, portHandleCount);
|
_ports.Keys.CopyTo(handles, 1);
|
||||||
|
|
||||||
|
_sessions.Keys.CopyTo(handles, portHandleCount + 1);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -227,8 +235,7 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We still need a timeout here to allow the service to pick up and listen new sessions...
|
var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, -1);
|
||||||
var rc = _context.Syscall.ReplyAndReceive(out int signaledIndex, handles.AsSpan(0, handleCount), replyTargetHandle, 1000000L);
|
|
||||||
|
|
||||||
_selfThread.HandlePostSyscall();
|
_selfThread.HandlePostSyscall();
|
||||||
|
|
||||||
@@ -239,7 +246,7 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
|
|
||||||
replyTargetHandle = 0;
|
replyTargetHandle = 0;
|
||||||
|
|
||||||
if (rc == Result.Success && signaledIndex >= portHandleCount)
|
if (rc == Result.Success && signaledIndex >= portHandleCount + 1)
|
||||||
{
|
{
|
||||||
// We got a IPC request, process it, pass to the appropriate service if needed.
|
// We got a IPC request, process it, pass to the appropriate service if needed.
|
||||||
int signaledHandle = handles[signaledIndex];
|
int signaledHandle = handles[signaledIndex];
|
||||||
@@ -253,24 +260,32 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
{
|
{
|
||||||
if (rc == Result.Success)
|
if (rc == Result.Success)
|
||||||
{
|
{
|
||||||
// We got a new connection, accept the session to allow servicing future requests.
|
if (signaledIndex > 0)
|
||||||
if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success)
|
|
||||||
{
|
{
|
||||||
bool handleWriteLockTaken = false;
|
// We got a new connection, accept the session to allow servicing future requests.
|
||||||
try
|
if (_context.Syscall.AcceptSession(out int serverSessionHandle, handles[signaledIndex]) == Result.Success)
|
||||||
{
|
{
|
||||||
handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
|
bool handleWriteLockTaken = false;
|
||||||
IpcService obj = _ports[handles[signaledIndex]].Invoke();
|
try
|
||||||
_sessions.Add(serverSessionHandle, obj);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (handleWriteLockTaken)
|
|
||||||
{
|
{
|
||||||
_handleLock.ExitWriteLock();
|
handleWriteLockTaken = _handleLock.TryEnterWriteLock(Timeout.Infinite);
|
||||||
|
IpcService obj = _ports[handles[signaledIndex]].Invoke();
|
||||||
|
_sessions.Add(serverSessionHandle, obj);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (handleWriteLockTaken)
|
||||||
|
{
|
||||||
|
_handleLock.ExitWriteLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The _wakeEvent signalled, which means we have a new session.
|
||||||
|
_wakeEvent.WritableEvent.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
|
_selfProcess.CpuMemory.Write(messagePtr + 0x0, 0);
|
||||||
@@ -499,6 +514,8 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
|
|
||||||
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
|
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
|
||||||
{
|
{
|
||||||
|
_selfProcess.HandleTable.CloseHandle(_wakeHandle);
|
||||||
|
|
||||||
foreach (IpcService service in _sessions.Values)
|
foreach (IpcService service in _sessions.Values)
|
||||||
{
|
{
|
||||||
(service as IDisposable)?.Dispose();
|
(service as IDisposable)?.Dispose();
|
||||||
|
@@ -2,21 +2,31 @@
|
|||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
|
using LibHac.FsSystem;
|
||||||
using LibHac.Loader;
|
using LibHac.Loader;
|
||||||
using LibHac.Ncm;
|
using LibHac.Ncm;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
|
using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using LibHac.Tools.Ncm;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ApplicationId = LibHac.Ncm.ApplicationId;
|
using ApplicationId = LibHac.Ncm.ApplicationId;
|
||||||
|
using ContentType = LibHac.Ncm.ContentType;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
{
|
{
|
||||||
static class NcaExtensions
|
public static class NcaExtensions
|
||||||
{
|
{
|
||||||
|
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
|
||||||
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
|
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
|
||||||
{
|
{
|
||||||
// Extract RomFs and ExeFs from NCA.
|
// Extract RomFs and ExeFs from NCA.
|
||||||
@@ -47,7 +57,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
|||||||
nacpData = controlNca.GetNacp(device);
|
nacpData = controlNca.GetNacp(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update.
|
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update.
|
||||||
|
|
||||||
// Load program 0 control NCA as we are going to need it for display version.
|
// Load program 0 control NCA as we are going to need it for display version.
|
||||||
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
|
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
|
||||||
@@ -86,6 +96,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
|||||||
return processResult;
|
return processResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ulong GetProgramIdBase(this Nca nca)
|
||||||
|
{
|
||||||
|
return nca.Header.TitleId & ~0x1FFFUL;
|
||||||
|
}
|
||||||
|
|
||||||
public static int GetProgramIndex(this Nca nca)
|
public static int GetProgramIndex(this Nca nca)
|
||||||
{
|
{
|
||||||
return (int)(nca.Header.TitleId & 0xF);
|
return (int)(nca.Header.TitleId & 0xF);
|
||||||
@@ -96,6 +111,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
|||||||
return nca.Header.ContentType == NcaContentType.Program;
|
return nca.Header.ContentType == NcaContentType.Program;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsMain(this Nca nca)
|
||||||
|
{
|
||||||
|
return nca.IsProgram() && !nca.IsPatch();
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsPatch(this Nca nca)
|
public static bool IsPatch(this Nca nca)
|
||||||
{
|
{
|
||||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||||
@@ -108,6 +128,56 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
|||||||
return nca.Header.ContentType == NcaContentType.Control;
|
return nca.Header.ContentType == NcaContentType.Control;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath)
|
||||||
|
{
|
||||||
|
updatePath = "(unknown)";
|
||||||
|
|
||||||
|
// Load Update NCAs.
|
||||||
|
Nca updatePatchNca = null;
|
||||||
|
Nca updateControlNca = null;
|
||||||
|
|
||||||
|
// Clear the program index part.
|
||||||
|
ulong titleIdBase = mainNca.GetProgramIdBase();
|
||||||
|
|
||||||
|
// Load update information if exists.
|
||||||
|
string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
|
||||||
|
if (File.Exists(titleUpdateMetadataPath))
|
||||||
|
{
|
||||||
|
updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
||||||
|
if (File.Exists(updatePath))
|
||||||
|
{
|
||||||
|
var updateFile = new FileStream(updatePath, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
|
IFileSystem updatePartitionFileSystem;
|
||||||
|
|
||||||
|
if (Path.GetExtension(updatePath).ToLower() == ".xci")
|
||||||
|
{
|
||||||
|
updatePartitionFileSystem = new Xci(fileSystem.KeySet, updateFile.AsStorage()).OpenPartition(XciPartitionType.Secure);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PartitionFileSystem pfsTemp = new();
|
||||||
|
pfsTemp.Initialize(updateFile.AsStorage()).ThrowIfFailure();
|
||||||
|
updatePartitionFileSystem = pfsTemp;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((ulong updateTitleId, ContentCollection content) in updatePartitionFileSystem.GetUpdateData(fileSystem, checkLevel))
|
||||||
|
{
|
||||||
|
if ((updateTitleId & ~0x1FFFUL) != titleIdBase)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex);
|
||||||
|
updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (updatePatchNca, updateControlNca);
|
||||||
|
}
|
||||||
|
|
||||||
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
|
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
|
||||||
{
|
{
|
||||||
IFileSystem exeFs = null;
|
IFileSystem exeFs = null;
|
||||||
@@ -172,5 +242,31 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
|||||||
|
|
||||||
return nacpData;
|
return nacpData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType)
|
||||||
|
{
|
||||||
|
string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt";
|
||||||
|
using var cnmtFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Result result = cnmtNca.OpenFileSystem(0, checkLevel)
|
||||||
|
.OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read);
|
||||||
|
|
||||||
|
if (result.IsSuccess())
|
||||||
|
{
|
||||||
|
return new Cnmt(cnmtFile.Release().AsStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HorizonResultException ex)
|
||||||
|
{
|
||||||
|
if (!ResultFs.PathNotFound.Includes(ex.ResultValue))
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"Failed get cnmt for '{cnmtNca.Header.TitleId:x16}' from nca: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,26 +1,91 @@
|
|||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
|
using LibHac.Common.Keys;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
using LibHac.Fs.Fsa;
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.FsSystem;
|
using LibHac.FsSystem;
|
||||||
|
using LibHac.Ncm;
|
||||||
using LibHac.Tools.Fs;
|
using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using LibHac.Tools.Ncm;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using ContentType = LibHac.Ncm.ContentType;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
||||||
{
|
{
|
||||||
public static class PartitionFileSystemExtensions
|
public static class PartitionFileSystemExtensions
|
||||||
{
|
{
|
||||||
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
|
||||||
|
|
||||||
internal static (bool, ProcessResult) TryLoad(this PartitionFileSystem partitionFileSystem, Switch device, string path, out string errorMessage)
|
public static Dictionary<ulong, ContentCollection> GetApplicationData(this IFileSystem partitionFileSystem,
|
||||||
|
VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel)
|
||||||
|
{
|
||||||
|
fileSystem.ImportTickets(partitionFileSystem);
|
||||||
|
|
||||||
|
var programs = new Dictionary<ulong, ContentCollection>();
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca"))
|
||||||
|
{
|
||||||
|
Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Application);
|
||||||
|
|
||||||
|
if (cnmt == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentCollection content = new(partitionFileSystem, cnmt);
|
||||||
|
|
||||||
|
if (content.Type != ContentMetaType.Application)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
programs.TryAdd(content.ApplicationId, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return programs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<ulong, ContentCollection> GetUpdateData(this IFileSystem partitionFileSystem,
|
||||||
|
VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel)
|
||||||
|
{
|
||||||
|
fileSystem.ImportTickets(partitionFileSystem);
|
||||||
|
|
||||||
|
var programs = new Dictionary<ulong, ContentCollection>();
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca"))
|
||||||
|
{
|
||||||
|
Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, ContentMetaType.Patch);
|
||||||
|
|
||||||
|
if (cnmt == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentCollection content = new(partitionFileSystem, cnmt);
|
||||||
|
|
||||||
|
if (content.Type != ContentMetaType.Patch)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
programs.TryAdd(content.ApplicationId, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return programs;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static (bool, ProcessResult) TryLoad<TMetaData, TFormat, THeader, TEntry>(this PartitionFileSystemCore<TMetaData, TFormat, THeader, TEntry> partitionFileSystem, Switch device, string path, ulong titleId, out string errorMessage)
|
||||||
|
where TMetaData : PartitionFileSystemMetaCore<TFormat, THeader, TEntry>, new()
|
||||||
|
where TFormat : IPartitionFileSystemFormat
|
||||||
|
where THeader : unmanaged, IPartitionFileSystemHeader
|
||||||
|
where TEntry : unmanaged, IPartitionFileSystemEntry
|
||||||
{
|
{
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
|
|
||||||
@@ -31,31 +96,22 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem);
|
Dictionary<ulong, ContentCollection> applications = partitionFileSystem.GetApplicationData(device.FileSystem, device.System.FsIntegrityCheckLevel);
|
||||||
|
|
||||||
// TODO: To support multi-games container, this should use CNMT NCA instead.
|
if (titleId == 0)
|
||||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
{
|
||||||
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
|
foreach ((ulong _, ContentCollection content) in applications)
|
||||||
|
|
||||||
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
|
|
||||||
{
|
{
|
||||||
continue;
|
mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
|
||||||
}
|
controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
|
||||||
|
break;
|
||||||
if (nca.IsPatch())
|
|
||||||
{
|
|
||||||
patchNca = nca;
|
|
||||||
}
|
|
||||||
else if (nca.IsProgram())
|
|
||||||
{
|
|
||||||
mainNca = nca;
|
|
||||||
}
|
|
||||||
else if (nca.IsControl())
|
|
||||||
{
|
|
||||||
controlNca = nca;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (applications.TryGetValue(titleId, out ContentCollection content))
|
||||||
|
{
|
||||||
|
mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
|
||||||
|
controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
|
||||||
|
}
|
||||||
|
|
||||||
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
|
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
|
||||||
}
|
}
|
||||||
@@ -75,53 +131,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
|||||||
return (false, ProcessResult.Failed);
|
return (false, ProcessResult.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Update NCAs.
|
(Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _);
|
||||||
Nca updatePatchNca = null;
|
|
||||||
Nca updateControlNca = null;
|
|
||||||
|
|
||||||
if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
|
|
||||||
{
|
|
||||||
// Clear the program index part.
|
|
||||||
titleIdBase &= ~0xFUL;
|
|
||||||
|
|
||||||
// Load update information if exists.
|
|
||||||
string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
|
|
||||||
if (File.Exists(titleUpdateMetadataPath))
|
|
||||||
{
|
|
||||||
string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
|
|
||||||
if (File.Exists(updatePath))
|
|
||||||
{
|
|
||||||
PartitionFileSystem updatePartitionFileSystem = new(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage());
|
|
||||||
|
|
||||||
device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
|
|
||||||
|
|
||||||
// TODO: This should use CNMT NCA instead.
|
|
||||||
foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
|
||||||
Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
|
|
||||||
|
|
||||||
if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nca.IsProgram())
|
|
||||||
{
|
|
||||||
updatePatchNca = nca;
|
|
||||||
}
|
|
||||||
else if (nca.IsControl())
|
|
||||||
{
|
|
||||||
updateControlNca = nca;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatePatchNca != null)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
@@ -163,18 +173,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
|
|||||||
return (true, mainNca.Load(device, patchNca, controlNca));
|
return (true, mainNca.Load(device, patchNca, controlNca));
|
||||||
}
|
}
|
||||||
|
|
||||||
errorMessage = "Unable to load: Could not find Main NCA";
|
errorMessage = $"Unable to load: Could not find Main NCA for title \"{titleId:X16}\"";
|
||||||
|
|
||||||
return (false, ProcessResult.Failed);
|
return (false, ProcessResult.Failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path)
|
public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path)
|
||||||
{
|
{
|
||||||
using var ncaFile = new UniqueRef<IFile>();
|
using var ncaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage());
|
return new Nca(keySet, ncaFile.Release().AsStorage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||||||
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
|
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadXci(string path)
|
public bool LoadXci(string path, ulong titleId)
|
||||||
{
|
{
|
||||||
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
|
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
|
||||||
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
|
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
|
||||||
@@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage);
|
(bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, titleId, out string errorMessage);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
{
|
{
|
||||||
@@ -66,12 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadNsp(string path)
|
public bool LoadNsp(string path, ulong titleId)
|
||||||
{
|
{
|
||||||
FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
FileStream file = new(path, FileMode.Open, FileAccess.Read);
|
||||||
PartitionFileSystem partitionFileSystem = new(file.AsStorage());
|
PartitionFileSystem partitionFileSystem = new();
|
||||||
|
partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||||
|
|
||||||
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage);
|
(bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, titleId, out string errorMessage);
|
||||||
|
|
||||||
if (processResult.ProcessId == 0)
|
if (processResult.ProcessId == 0)
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
using LibHac.Account;
|
using LibHac.Account;
|
||||||
using LibHac.Common;
|
using LibHac.Common;
|
||||||
using LibHac.Fs;
|
using LibHac.Fs;
|
||||||
|
using LibHac.Fs.Fsa;
|
||||||
using LibHac.Fs.Shim;
|
using LibHac.Fs.Shim;
|
||||||
using LibHac.FsSystem;
|
|
||||||
using LibHac.Loader;
|
using LibHac.Loader;
|
||||||
using LibHac.Ncm;
|
using LibHac.Ncm;
|
||||||
using LibHac.Ns;
|
using LibHac.Ns;
|
||||||
@@ -33,7 +33,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||||||
// TODO: Remove this workaround when ASLR is implemented.
|
// TODO: Remove this workaround when ASLR is implemented.
|
||||||
private const ulong CodeStartOffset = 0x500000UL;
|
private const ulong CodeStartOffset = 0x500000UL;
|
||||||
|
|
||||||
public static LibHac.Result RegisterProgramMapInfo(Switch device, PartitionFileSystem partitionFileSystem)
|
public static LibHac.Result RegisterProgramMapInfo(Switch device, IFileSystem partitionFileSystem)
|
||||||
{
|
{
|
||||||
ulong applicationId = 0;
|
ulong applicationId = 0;
|
||||||
int programCount = 0;
|
int programCount = 0;
|
||||||
@@ -42,15 +42,14 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||||
{
|
{
|
||||||
Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
|
Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath);
|
||||||
|
|
||||||
if (!nca.IsProgram() && nca.IsPatch())
|
if (!nca.IsProgram())
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong currentProgramId = nca.Header.TitleId;
|
ulong currentMainProgramId = nca.GetProgramIdBase();
|
||||||
ulong currentMainProgramId = currentProgramId & ~0xFFFul;
|
|
||||||
|
|
||||||
if (applicationId == 0 && currentMainProgramId != 0)
|
if (applicationId == 0 && currentMainProgramId != 0)
|
||||||
{
|
{
|
||||||
@@ -67,7 +66,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasIndex[(int)(currentProgramId & 0xF)] = true;
|
hasIndex[nca.GetProgramIndex()] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (programCount == 0)
|
if (programCount == 0)
|
||||||
|
@@ -27,6 +27,7 @@
|
|||||||
<PackageReference Include="SixLabors.ImageSharp" />
|
<PackageReference Include="SixLabors.ImageSharp" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" />
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
|
||||||
|
<PackageReference Include="NetCoreServer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Due to Concentus. -->
|
<!-- Due to Concentus. -->
|
||||||
|
@@ -72,9 +72,9 @@ namespace Ryujinx.HLE
|
|||||||
return Processes.LoadUnpackedNca(exeFsDir, romFsFile);
|
return Processes.LoadUnpackedNca(exeFsDir, romFsFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadXci(string xciFile)
|
public bool LoadXci(string xciFile, ulong titleId = 0)
|
||||||
{
|
{
|
||||||
return Processes.LoadXci(xciFile);
|
return Processes.LoadXci(xciFile, titleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadNca(string ncaFile)
|
public bool LoadNca(string ncaFile)
|
||||||
@@ -82,9 +82,9 @@ namespace Ryujinx.HLE
|
|||||||
return Processes.LoadNca(ncaFile);
|
return Processes.LoadNca(ncaFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadNsp(string nspFile)
|
public bool LoadNsp(string nspFile, ulong titleId = 0)
|
||||||
{
|
{
|
||||||
return Processes.LoadNsp(nspFile);
|
return Processes.LoadNsp(nspFile, titleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadProgram(string fileName)
|
public bool LoadProgram(string fileName)
|
||||||
|
@@ -455,6 +455,11 @@ namespace Ryujinx.Memory
|
|||||||
return _pageTable.Read(va) + (nuint)(va & PageMask);
|
return _pageTable.Read(va) + (nuint)(va & PageMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
{
|
{
|
||||||
|
@@ -104,6 +104,12 @@ namespace Ryujinx.Memory
|
|||||||
/// <returns>True if the data was changed, false otherwise</returns>
|
/// <returns>True if the data was changed, false otherwise</returns>
|
||||||
bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data);
|
bool WriteWithRedundancyCheck(ulong va, ReadOnlySpan<byte> data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fills the specified memory region with the value specified in <paramref name="value"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="va">Virtual address to fill the value into</param>
|
||||||
|
/// <param name="size">Size of the memory region to fill</param>
|
||||||
|
/// <param name="value">Value to fill with</param>
|
||||||
void Fill(ulong va, ulong size, byte value)
|
void Fill(ulong va, ulong size, byte value)
|
||||||
{
|
{
|
||||||
const int MaxChunkSize = 1 << 24;
|
const int MaxChunkSize = 1 << 24;
|
||||||
@@ -194,6 +200,14 @@ namespace Ryujinx.Memory
|
|||||||
/// <param name="exemptId">Optional ID of the handles that should not be signalled</param>
|
/// <param name="exemptId">Optional ID of the handles that should not be signalled</param>
|
||||||
void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null);
|
void SignalMemoryTracking(ulong va, ulong size, bool write, bool precise = false, int? exemptId = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reprotect a region of virtual memory for guest access.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="va">Virtual address base</param>
|
||||||
|
/// <param name="size">Size of the region to protect</param>
|
||||||
|
/// <param name="protection">Memory protection to set</param>
|
||||||
|
void Reprotect(ulong va, ulong size, MemoryPermission protection);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reprotect a region of virtual memory for tracking.
|
/// Reprotect a region of virtual memory for tracking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@@ -102,6 +102,11 @@ namespace Ryujinx.Tests.Memory
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Reprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
public void TrackingReprotect(ulong va, ulong size, MemoryPermission protection)
|
||||||
{
|
{
|
||||||
OnProtect?.Invoke(va, size, protection);
|
OnProtect?.Invoke(va, size, protection);
|
||||||
|
@@ -9,8 +9,9 @@ using LibHac.Tools.FsSystem;
|
|||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
using Ryujinx.HLE.Loaders.Processes.Extensions;
|
||||||
|
using Ryujinx.Ui.Common.Helper;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
@@ -20,35 +21,28 @@ namespace Ryujinx.Ui.App.Common
|
|||||||
{
|
{
|
||||||
public bool Favorite { get; set; }
|
public bool Favorite { get; set; }
|
||||||
public byte[] Icon { get; set; }
|
public byte[] Icon { get; set; }
|
||||||
public string TitleName { get; set; }
|
public string Name { get; set; } = "Unknown";
|
||||||
public string TitleId { get; set; }
|
public ulong Id { get; set; }
|
||||||
public string Developer { get; set; }
|
public string Developer { get; set; } = "Unknown";
|
||||||
public string Version { get; set; }
|
public string Version { get; set; } = "0";
|
||||||
public string TimePlayed { get; set; }
|
public TimeSpan TimePlayed { get; set; }
|
||||||
public double TimePlayedNum { get; set; }
|
|
||||||
public DateTime? LastPlayed { get; set; }
|
public DateTime? LastPlayed { get; set; }
|
||||||
public string FileExtension { get; set; }
|
public string FileExtension { get; set; }
|
||||||
public string FileSize { get; set; }
|
public long FileSize { get; set; }
|
||||||
public double FileSizeBytes { get; set; }
|
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
|
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
|
||||||
public string LastPlayedString
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!LastPlayed.HasValue)
|
|
||||||
{
|
|
||||||
// TODO: maybe put localized string here instead of just "Never"
|
|
||||||
return "Never";
|
|
||||||
}
|
|
||||||
|
|
||||||
return LastPlayed.Value.ToLocalTime().ToString(CultureInfo.CurrentCulture);
|
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
|
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
||||||
|
|
||||||
|
[JsonIgnore] public string IdString => Id.ToString("x16");
|
||||||
|
|
||||||
|
[JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL;
|
||||||
|
|
||||||
|
public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
|
||||||
{
|
{
|
||||||
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
|
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
@@ -65,7 +59,7 @@ namespace Ryujinx.Ui.App.Common
|
|||||||
|
|
||||||
if (extension is ".nsp" or ".xci")
|
if (extension is ".nsp" or ".xci")
|
||||||
{
|
{
|
||||||
PartitionFileSystem pfs;
|
IFileSystem pfs;
|
||||||
|
|
||||||
if (extension == ".xci")
|
if (extension == ".xci")
|
||||||
{
|
{
|
||||||
@@ -75,7 +69,9 @@ namespace Ryujinx.Ui.App.Common
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
pfs = new PartitionFileSystem(file.AsStorage());
|
var pfsTemp = new PartitionFileSystem();
|
||||||
|
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
|
||||||
|
pfs = pfsTemp;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
|
||||||
@@ -115,7 +111,7 @@ namespace Ryujinx.Ui.App.Common
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
(Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
|
(Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _);
|
||||||
|
|
||||||
if (updatePatchNca != null)
|
if (updatePatchNca != null)
|
||||||
{
|
{
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user