feat: implement real applets

Co-authored-by: Alula <6276139+alula@users.noreply.github.com>
This commit is contained in:
Jacobwasbeast 2025-02-11 08:09:51 -06:00
parent c218305bc0
commit 3b568b61ba
26 changed files with 1091 additions and 10 deletions

View File

@ -8,7 +8,7 @@ namespace ARMeilleure.Signal
{
public static class NativeSignalHandlerGenerator
{
public const int MaxTrackedRanges = 16;
public const int MaxTrackedRanges = 256;
private const int StructAddressOffset = 0;
private const int StructWriteOffset = 4;

View File

@ -25,9 +25,6 @@ namespace Ryujinx.HLE.HOS.Applets
case AppletId.LibAppletShop:
case AppletId.LibAppletOff:
return new BrowserApplet();
case AppletId.MiiEdit:
Logger.Warning?.Print(LogClass.Application, $"Please use the MiiEdit inside File/Open Applet");
return new DummyApplet(system);
case AppletId.Cabinet:
return new CabinetApplet(system);
}

View File

@ -576,6 +576,35 @@ namespace Ryujinx.HLE.HOS.Applets
}
}
}
public bool IsLaunchedAsReal(ulong pid)
{
RealApplet applet = null;
lock (_lock)
{
if (_applets.TryGetValue(pid, out applet))
{
if (_applets.Count > 1)
{
return true;
}
else
{
if (applet==_homeMenu||applet==_overlayDisp)
{
return false;
}
}
}
}
return false;
}
public bool HasAnyApplets()
{
return _applets.Count > 0;
}
}
internal class ButtonPressTracker

View File

@ -7,7 +7,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Ipc
{
class KBufferDescriptorTable
{
private const int MaxInternalBuffersCount = 8;
private const int MaxInternalBuffersCount = 256;
private readonly List<KBufferDescriptor> _sendBufferDescriptors;
private readonly List<KBufferDescriptor> _receiveBufferDescriptors;

View File

@ -88,8 +88,15 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService
// OpenLibraryAppletSelfAccessor() -> object<nn::am::service::ILibraryAppletSelfAccessor>
public ResultCode OpenLibraryAppletSelfAccessor(ServiceCtx context)
{
MakeObject(context, new ILibraryAppletSelfAccessor(context));
if (context.Device.System.WindowSystem.IsLaunchedAsReal(_pid))
{
MakeObject(context, new ILibraryRealAppletSelfAccessor(context, _pid));
}
else
{
MakeObject(context, new ILibraryAppletSelfAccessor(context));
}
return ResultCode.Success;
}

View File

@ -0,0 +1,323 @@
using LibHac.Ncm;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Sdk.Applet;
using System;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletCreator
{
class ILibraryRealAppletAccessor : DisposableIpcService
{
private readonly KernelContext _kernelContext;
private readonly ulong _callerPid;
private readonly RealAppletId _appletId;
private RealApplet _applet;
private readonly AppletChannel _inChannel;
private readonly AppletChannel _outChannel;
private readonly AppletChannel _interactiveInChannel;
private readonly AppletChannel _interactiveOutChannel;
private readonly AppletChannel _contextChannel;
private readonly KEvent _stateChangedEvent;
private readonly KEvent _normalOutDataEvent;
private readonly KEvent _interactiveOutDataEvent;
private int _stateChangedEventHandle;
private int _normalOutDataEventHandle;
private int _interactiveOutDataEventHandle;
private int _indirectLayerHandle;
private ResultCode StartAppletProcess(Horizon system)
{
// TODO: use ns
var programId = RealApplet.GetProgramIdFromAppletId(_appletId);
string contentPath = system.ContentManager.GetInstalledContentPath(programId, StorageId.BuiltInSystem, NcaContentType.Program);
if (contentPath.Length == 0)
{
return ResultCode.AppletLaunchFailed;
}
if (contentPath.StartsWith("@SystemContent"))
{
contentPath = FileSystem.VirtualFileSystem.SwitchPathToSystemPath(contentPath);
}
if (!system.Device.Processes.LoadNca(contentPath, out var Process))
{
return ResultCode.AppletLaunchFailed;
}
_applet = system.WindowSystem.TrackProcess(Process.ProcessId, _callerPid, false);
_applet.AppletStateChanged += OnAppletStateChanged;
_applet.AppletState.LaunchableEvent.ReadableEvent.Signal();
return ResultCode.Success;
}
public ILibraryRealAppletAccessor(RealAppletId appletId, Horizon system, ulong callerPid)
{
_kernelContext = system.KernelContext;
_stateChangedEvent = new KEvent(system.KernelContext);
_normalOutDataEvent = new KEvent(system.KernelContext);
_interactiveOutDataEvent = new KEvent(system.KernelContext);
_callerPid = callerPid;
_appletId = appletId;
_inChannel = new AppletChannel();
_outChannel = new AppletChannel();
_interactiveInChannel = new AppletChannel();
_interactiveOutChannel = new AppletChannel();
_contextChannel = new AppletChannel();
_outChannel.DataAvailable += OnNormalOutData;
_interactiveOutChannel.DataAvailable += OnInteractiveOutData;
}
private void OnAppletStateChanged(object sender, EventArgs e)
{
_stateChangedEvent.WritableEvent.Signal();
}
private void OnNormalOutData(object sender, EventArgs e)
{
_normalOutDataEvent.WritableEvent.Signal();
}
private void OnInteractiveOutData(object sender, EventArgs e)
{
_interactiveOutDataEvent.WritableEvent.Signal();
}
[CommandCmif(0)]
// GetAppletStateChangedEvent() -> handle<copy>
public ResultCode GetAppletStateChangedEvent(ServiceCtx context)
{
if (_stateChangedEventHandle == 0)
{
if (context.Process.HandleTable.GenerateHandle(_stateChangedEvent.ReadableEvent, out _stateChangedEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_stateChangedEventHandle);
return ResultCode.Success;
}
[CommandCmif(10)]
// Start()
public ResultCode Start(ServiceCtx context)
{
var result = StartAppletProcess(context.Device.System);
if (result != ResultCode.Success)
{
return result;
}
return (ResultCode)_applet.Start(_inChannel, _outChannel, _interactiveInChannel, _interactiveOutChannel, _contextChannel);
}
[CommandCmif(20)]
// RequestExit()
public ResultCode RequestExit(ServiceCtx context)
{
_applet.ProcessHandle.SetActivity(false);
_applet.AppletState.OnExitRequested();
_applet?.ProcessHandle.Terminate();
Logger.Stub?.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
[CommandCmif(25)]
// Terminate()
public ResultCode Terminate(ServiceCtx context)
{
_applet.ProcessHandle.SetActivity(false);
_applet.AppletState.OnExitRequested();
_applet?.ProcessHandle.Terminate();
Logger.Stub?.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
[CommandCmif(30)]
// GetResult()
public ResultCode GetResult(ServiceCtx context)
{
if (_applet == null)
{
return ResultCode.LibraryAppletTerminated;
}
return (ResultCode)_applet.GetResult();
}
[CommandCmif(60)]
// PresetLibraryAppletGpuTimeSliceZero()
public ResultCode PresetLibraryAppletGpuTimeSliceZero(ServiceCtx context)
{
// NOTE: This call reset two internal fields to 0 and one internal field to "true".
// It seems to be used only with software keyboard inline.
// Since we doesn't support applets for now, it's fine to stub it.
Logger.Stub?.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
[CommandCmif(100)]
// PushInData(object<nn::am::service::IStorage>)
public ResultCode PushInData(ServiceCtx context)
{
IStorage data = GetObject<IStorage>(context, 0);
_inChannel.PushData(data.Data);
return ResultCode.Success;
}
[CommandCmif(101)]
// PopOutData() -> object<nn::am::service::IStorage>
public ResultCode PopOutData(ServiceCtx context)
{
if (_outChannel.TryPopData(out byte[] data))
{
MakeObject(context, new IStorage(data));
_normalOutDataEvent.WritableEvent.Clear();
return ResultCode.Success;
}
return ResultCode.NotAvailable;
}
[CommandCmif(103)]
// PushInteractiveInData(object<nn::am::service::IStorage>)
public ResultCode PushInteractiveInData(ServiceCtx context)
{
IStorage data = GetObject<IStorage>(context, 0);
_interactiveInChannel.PushData(data.Data);
return ResultCode.Success;
}
[CommandCmif(104)]
// PopInteractiveOutData() -> object<nn::am::service::IStorage>
public ResultCode PopInteractiveOutData(ServiceCtx context)
{
if (_interactiveOutChannel.TryPopData(out byte[] data))
{
MakeObject(context, new IStorage(data));
_interactiveOutDataEvent.WritableEvent.Clear();
return ResultCode.Success;
}
return ResultCode.NotAvailable;
}
[CommandCmif(105)]
// GetPopOutDataEvent() -> handle<copy>
public ResultCode GetPopOutDataEvent(ServiceCtx context)
{
if (_normalOutDataEventHandle == 0)
{
if (context.Process.HandleTable.GenerateHandle(_normalOutDataEvent.ReadableEvent, out _normalOutDataEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_normalOutDataEventHandle);
return ResultCode.Success;
}
[CommandCmif(106)]
// GetPopInteractiveOutDataEvent() -> handle<copy>
public ResultCode GetPopInteractiveOutDataEvent(ServiceCtx context)
{
if (_interactiveOutDataEventHandle == 0)
{
if (context.Process.HandleTable.GenerateHandle(_interactiveOutDataEvent.ReadableEvent, out _interactiveOutDataEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_interactiveOutDataEventHandle);
return ResultCode.Success;
}
[CommandCmif(110)]
// NeedsToExitProcess()
public ResultCode NeedsToExitProcess(ServiceCtx context)
{
return ResultCode.Stubbed;
}
[CommandCmif(150)]
// RequestForAppletToGetForeground()
public ResultCode RequestForAppletToGetForeground(ServiceCtx context)
{
return ResultCode.Stubbed;
}
[CommandCmif(160)] // 2.0.0+
// GetIndirectLayerConsumerHandle() -> u64 indirect_layer_consumer_handle
public ResultCode GetIndirectLayerConsumerHandle(ServiceCtx context)
{
if (_applet == null)
{
return ResultCode.LibraryAppletTerminated;
}
_indirectLayerHandle = _applet.AppletState.IndirectLayerHandles.Add(_applet);
context.ResponseData.Write((ulong)_indirectLayerHandle);
return ResultCode.Success;
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
if (_stateChangedEventHandle != 0)
{
_kernelContext.Syscall.CloseHandle(_stateChangedEventHandle);
}
if (_normalOutDataEventHandle != 0)
{
_kernelContext.Syscall.CloseHandle(_normalOutDataEventHandle);
}
if (_interactiveOutDataEventHandle != 0)
{
_kernelContext.Syscall.CloseHandle(_interactiveOutDataEventHandle);
}
_applet?.AppletState.IndirectLayerHandles.Delete(_indirectLayerHandle);
}
}
}
}

View File

@ -0,0 +1,274 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Sdk.Applet;
using System;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletProxy
{
class ILibraryRealAppletSelfAccessor : DisposableIpcService
{
private readonly KernelContext _kernelContext;
private readonly RealApplet _applet;
private readonly KEvent _normalInDataEvent;
private readonly KEvent _interactiveInDataEvent;
private int _normalInDataEventHandle;
private int _interactiveInDataEventHandle;
public ILibraryRealAppletSelfAccessor(ServiceCtx context, ulong pid)
{
var system = context.Device.System;
_kernelContext = system.KernelContext;
_applet = system.WindowSystem.GetByAruId(pid);
_normalInDataEvent = new KEvent(system.KernelContext);
_interactiveInDataEvent = new KEvent(system.KernelContext);
_applet.InChannel.DataAvailable += OnNormalInData;
_applet.InteractiveInChannel.DataAvailable += OnInteractiveInData;
}
private void OnNormalInData(object sender, EventArgs e)
{
_normalInDataEvent.WritableEvent.Signal();
}
private void OnInteractiveInData(object sender, EventArgs e)
{
_interactiveInDataEvent.WritableEvent.Signal();
}
[CommandCmif(0)]
// PopInData() -> object<nn::am::service::IStorage>
public ResultCode PopInData(ServiceCtx context)
{
byte[] appletData;
if (!_applet.InChannel.TryPopData(out appletData))
{
return ResultCode.NotAvailable;
}
if (appletData.Length == 0)
{
return ResultCode.NotAvailable;
}
MakeObject(context, new IStorage(appletData));
return ResultCode.Success;
}
[CommandCmif(1)]
public ResultCode PushOutData(ServiceCtx context)
{
if (_applet != null)
{
IStorage data = GetObject<IStorage>(context, 0);
_applet.OutChannel.PushData(data.Data);
}
return ResultCode.Success;
}
[CommandCmif(2)]
public ResultCode PopInteractiveInData(ServiceCtx context)
{
byte[] appletData;
if (!_applet.InteractiveInChannel.TryPopData(out appletData))
{
return ResultCode.NotAvailable;
}
if (appletData.Length == 0)
{
return ResultCode.NotAvailable;
}
MakeObject(context, new IStorage(appletData));
return ResultCode.Success;
}
[CommandCmif(3)]
public ResultCode PushInteractiveOutData(ServiceCtx context)
{
if (_applet != null)
{
IStorage data = GetObject<IStorage>(context, 0);
_applet.InteractiveOutChannel.PushData(data.Data);
}
return ResultCode.Success;
}
[CommandCmif(5)]
public ResultCode GetPopInDataEvent(ServiceCtx context)
{
if (_normalInDataEventHandle == 0)
{
if (context.Process.HandleTable.GenerateHandle(_normalInDataEvent.ReadableEvent, out _normalInDataEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_normalInDataEventHandle);
return ResultCode.Success;
}
[CommandCmif(6)]
public ResultCode GetPopInteractiveInDataEvent(ServiceCtx context)
{
if (_interactiveInDataEventHandle == 0)
{
if (context.Process.HandleTable.GenerateHandle(_interactiveInDataEvent.ReadableEvent, out _interactiveInDataEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_interactiveInDataEventHandle);
return ResultCode.Success;
}
[CommandCmif(10)]
public ResultCode ExitProcessAndReturn(ServiceCtx context)
{
_applet.ProcessHandle.Terminate();
return ResultCode.Success;
}
[CommandCmif(11)]
// GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo
public ResultCode GetLibraryAppletInfo(ServiceCtx context)
{
LibraryAppletInfo libraryAppletInfo = new();
libraryAppletInfo.AppletId = (AppletId)_applet.AppletId;
libraryAppletInfo.LibraryAppletMode = _applet.LibraryAppletMode;
context.ResponseData.WriteStruct(libraryAppletInfo);
return ResultCode.Success;
}
[CommandCmif(12)]
// GetMainAppletIdentityInfo() -> nn::am::service::AppletIdentityInfo
public ResultCode GetMainAppletIdentityInfo(ServiceCtx context)
{
AppletIdentifyInfo appletIdentifyInfo = new()
{
AppletId = AppletId.QLaunch,
TitleId = 0x0100000000001000,
};
context.ResponseData.WriteStruct(appletIdentifyInfo);
return ResultCode.Success;
}
[CommandCmif(13)]
// CanUseApplicationCore() -> bool
public ResultCode CanUseApplicationCore(ServiceCtx context)
{
context.ResponseData.Write(false);
Logger.Stub?.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
[CommandCmif(14)]
// GetCallerAppletIdentityInfo() -> nn::am::service::AppletIdentityInfo
public ResultCode GetCallerAppletIdentityInfo(ServiceCtx context)
{
context.ResponseData.WriteStruct(GetCallerIdentity(_applet));
return ResultCode.Success;
}
[CommandCmif(30)]
// UnpopInData(nn::am::service::IStorage)
public ResultCode UnpopInData(ServiceCtx context)
{
IStorage data = GetObject<IStorage>(context, 0);
_applet.InChannel.InsertFrontData(data.Data);
return ResultCode.Success;
}
[CommandCmif(50)]
// ReportVisibleError(nn::err::ErrorCode)
public ResultCode ReportVisibleError(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
[CommandCmif(150)]
// ShouldSetGpuTimeSliceManually() -> bool
public ResultCode ShouldSetGpuTimeSliceManually(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceAm);
context.ResponseData.Write(false);
return ResultCode.Success;
}
[CommandCmif(160)]
// GetLibraryAppletInfoEx() -> u64 usually 0
public ResultCode GetLibraryAppletInfoEx(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceAm);
context.ResponseData.Write((ulong)0);
return ResultCode.Success;
}
private static AppletIdentifyInfo GetCallerIdentity(RealApplet applet)
{
if (applet.CallerApplet != null)
{
return new AppletIdentifyInfo
{
AppletId = (AppletId)applet.CallerApplet.AppletId,
TitleId = applet.CallerApplet.ProcessHandle.TitleId,
};
}
else
{
return new AppletIdentifyInfo
{
AppletId = AppletId.QLaunch,
TitleId = 0x0100000000001000
};
}
}
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
if (_normalInDataEventHandle != 0)
{
_kernelContext.Syscall.CloseHandle(_normalInDataEventHandle);
}
if (_interactiveInDataEventHandle != 0)
{
_kernelContext.Syscall.CloseHandle(_interactiveInDataEventHandle);
}
}
}
}
}

View File

@ -124,6 +124,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.Success;
}
[CommandCmif(7)]
[CommandCmif(24)]
[CommandCmif(26)]

View File

@ -22,7 +22,15 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
int libraryAppletMode = context.RequestData.ReadInt32();
#pragma warning restore IDE0059
MakeObject(context, new ILibraryAppletAccessor(appletId, context.Device.System, _pid));
if (ShouldBeReal(context.Device.System,appletId))
{
RealAppletId realAppletId = (RealAppletId)appletId;
MakeObject(context, new ILibraryRealAppletAccessor(realAppletId, context.Device.System, _pid));
}
else
{
MakeObject(context, new ILibraryAppletAccessor(appletId, context.Device.System, _pid));
}
return ResultCode.Success;
}
@ -95,5 +103,10 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.Success;
}
public bool ShouldBeReal(Horizon horizon, AppletId appletId)
{
return horizon.Device.Configuration.HostUIHandler.IsAppletReal((RealAppletId)appletId);
}
}
}

View File

@ -1,6 +1,7 @@
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.Horizon.Sdk.Applet;
namespace Ryujinx.HLE.UI
{
@ -71,5 +72,10 @@ namespace Ryujinx.HLE.UI
/// Gets the UI theme and returns true if is dark mode.
/// </summary>
bool IsDarkMode();
/// <summary>
/// Gets weather or not the applet is real in the system
/// </summary>
bool IsAppletReal(RealAppletId appletId);
}
}

View File

@ -5647,6 +5647,31 @@
"zh_TW": "日誌"
}
},
{
"ID": "SettingsTabRealApplets",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Real Applets",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsTabLoggingLogging",
"Translations": {

View File

@ -150,6 +150,24 @@ namespace Ryujinx.Headless
if (NeedsOverride(nameof(IgnoreControllerApplet)))
IgnoreControllerApplet = configurationState.System.IgnoreApplet;
if (NeedsOverride(nameof(MissingAppletsAsReal)))
MissingAppletsAsReal = configurationState.System.MissingAppletsAsReal;
if (NeedsOverride(nameof(SoftwareKeyboardIsReal)))
SoftwareKeyboardIsReal = configurationState.System.SoftwareKeyboardIsReal;
if (NeedsOverride(nameof(BrowserIsReal)))
BrowserIsReal = configurationState.System.BrowserIsReal;
if (NeedsOverride(nameof(ControllerIsReal)))
ControllerIsReal = configurationState.System.ControllerIsReal;
if (NeedsOverride(nameof(PlayerSelectIsReal)))
PlayerSelectIsReal = configurationState.System.PlayerSelectIsReal;
if (NeedsOverride(nameof(CabinetIsReal)))
CabinetIsReal = configurationState.System.CabinetIsReal;
return;
bool NeedsOverride(string argKey) => originalArgs.None(arg => arg.TrimStart('-').EqualsIgnoreCase(OptionName(argKey)));
@ -411,8 +429,26 @@ namespace Ryujinx.Headless
public bool IgnoreMissingServices { get; set; }
[Option("ignore-controller-applet", Required = false, Default = false, HelpText = "Enable ignoring the controller applet when your game loses connection to your controller.")]
public bool IgnoreControllerApplet { get; set; }
public bool IgnoreControllerApplet { get; set; }
[Option("missing-applets-as-real", Required = false, Default = false, HelpText = "Runs the missing applets as real applets.")]
public bool MissingAppletsAsReal { get; set; }
[Option("is-softwarekeyboard-real", Required = false, Default = false, HelpText = "Runs the software keyboard applets as real")]
public bool SoftwareKeyboardIsReal { get; set; }
[Option("is-browser-real", Required = false, Default = false, HelpText = "Runs the browser applets as real")]
public bool BrowserIsReal { get; set; }
[Option("is-controller-real", Required = false, Default = false, HelpText = "Runs the controller applets as real")]
public bool ControllerIsReal { get; set; }
[Option("is-playerselect-real", Required = false, Default = false, HelpText = "Runs the player select applets as real")]
public bool PlayerSelectIsReal { get; set; }
[Option("is-cabinet-real", Required = false, Default = false, HelpText = "Runs the cabinet applets as real")]
public bool CabinetIsReal { get; set; }
// Values
[Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)]

View File

@ -13,6 +13,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.HLE.UI;
using Ryujinx.Horizon.Sdk.Applet;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL2;
@ -568,5 +569,10 @@ namespace Ryujinx.Headless
{
return true;
}
public bool IsAppletReal(RealAppletId appletId)
{
return false;
}
}
}

View File

@ -172,5 +172,11 @@
<ItemGroup>
<Folder Include="Assets\Fonts\Mono\" />
</ItemGroup>
<ItemGroup>
<Compile Update="UI\Views\Settings\SettingsRealApplets.axaml.cs">
<DependentUpon>SettingsRealApplets.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project>

View File

@ -15,6 +15,7 @@ using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.HLE.UI;
using Ryujinx.Horizon.Sdk.Applet;
using System;
using System.Collections.ObjectModel;
using System.Linq;
@ -318,5 +319,36 @@ namespace Ryujinx.Ava.UI.Applet
{
return ConfigurationState.Instance.UI.BaseStyle.Value == "Dark";
}
public bool IsMissingAppletsAsReal()
{
return ConfigurationState.Instance.System.MissingAppletsAsReal;
}
public bool IsAppletReal(RealAppletId appletId)
{
bool softwareKeyboardReal = ConfigurationState.Instance.System.SoftwareKeyboardIsReal;
bool browserReal = ConfigurationState.Instance.System.BrowserIsReal;
bool controllerReal = ConfigurationState.Instance.System.ControllerIsReal;
bool playerSelectReal = ConfigurationState.Instance.System.PlayerSelectIsReal;
bool cabinetReal = ConfigurationState.Instance.System.CabinetIsReal;
bool missingAppletsAsReal = ConfigurationState.Instance.System.MissingAppletsAsReal;
switch (appletId)
{
case RealAppletId.LibraryAppletSwkbd:
return softwareKeyboardReal;
case RealAppletId.LibraryAppletWeb:
return browserReal;
case RealAppletId.LibraryAppletController:
return controllerReal;
case RealAppletId.LibraryAppletPlayerSelect:
return playerSelectReal;
case RealAppletId.LibraryAppletCabinet:
return cabinetReal;
default:
return missingAppletsAsReal;
}
}
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace Ryujinx.Ava.UI.Helpers
{
public class BoolToIndexConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? 0 : 1;
}
return 1;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is int index)
{
return index == 0;
}
return false;
}
}
}

View File

@ -0,0 +1,21 @@
using Avalonia.Data.Converters;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using Ryujinx.Ava.Utilities.Configuration;
using System;
using System.Globalization;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class SettingsRealAppsViewModel : BaseModel
{
private readonly SettingsViewModel _baseViewModel;
public SettingsRealAppsViewModel() {}
public SettingsRealAppsViewModel(SettingsViewModel settingsVm)
{
_baseViewModel = settingsVm;
}
}
}

View File

@ -190,6 +190,12 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableInternetAccess { get; set; }
public bool EnableFsIntegrityChecks { get; set; }
public bool IgnoreMissingServices { get; set; }
public bool MissingAppletsAsReal { get; set; }
public bool SoftwareKeyboardIsReal { get; set; }
public bool BrowserIsReal { get; set; }
public bool ControllerIsReal { get; set; }
public bool PlayerSelectIsReal { get; set; }
public bool CabinetIsReal { get; set; }
public MemoryConfiguration DramSize { get; set; }
public bool EnableShaderCache { get; set; }
public bool EnableTextureRecompression { get; set; }
@ -518,6 +524,13 @@ namespace Ryujinx.Ava.UI.ViewModels
DramSize = config.System.DramSize;
IgnoreMissingServices = config.System.IgnoreMissingServices;
IgnoreApplet = config.System.IgnoreApplet;
MissingAppletsAsReal = config.System.MissingAppletsAsReal;
SoftwareKeyboardIsReal = config.System.SoftwareKeyboardIsReal;
BrowserIsReal = config.System.BrowserIsReal;
ControllerIsReal = config.System.ControllerIsReal;
PlayerSelectIsReal = config.System.PlayerSelectIsReal;
CabinetIsReal = config.System.CabinetIsReal;
// CPU
EnablePptc = config.System.EnablePtc;
@ -622,6 +635,12 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.DramSize.Value = DramSize;
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
config.System.IgnoreApplet.Value = IgnoreApplet;
config.System.MissingAppletsAsReal.Value = MissingAppletsAsReal;
config.System.SoftwareKeyboardIsReal.Value = SoftwareKeyboardIsReal;
config.System.BrowserIsReal.Value = BrowserIsReal;
config.System.ControllerIsReal.Value = ControllerIsReal;
config.System.PlayerSelectIsReal.Value = PlayerSelectIsReal;
config.System.CabinetIsReal.Value = CabinetIsReal;
// CPU
config.System.EnablePtc.Value = EnablePptc;

View File

@ -0,0 +1,142 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsRealAppsView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
mc:Ignorable="d"
x:DataType="viewModels:SettingsViewModel">
<!-- Resources -->
<UserControl.Resources>
<helpers:BoolToIndexConverter x:Key="BoolToIndexConverter" />
</UserControl.Resources>
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel Margin="10"
HorizontalAlignment="Left"
Orientation="Vertical"
Spacing="10">
<!-- Header -->
<TextBlock HorizontalAlignment="Center"
Classes="h1"
Text="Real Applets" />
<TextBlock Foreground="{DynamicResource SecondaryTextColor}"
TextDecorations="Underline"
Text="This is a collection of real applet options." />
<!-- Missing Applets Row -->
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox Width="100"
SelectedIndex="{Binding MissingAppletsAsReal, Mode=TwoWay, Converter={StaticResource BoolToIndexConverter}}">
<ComboBoxItem Content="Real" />
<ComboBoxItem Content="Skip" />
</ComboBox>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Margin="10,0,0,0"
Text="Missing Applets" />
</Grid>
<!-- Software Keyboard Row -->
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox Width="100"
SelectedIndex="{Binding SoftwareKeyboardIsReal, Mode=TwoWay, Converter={StaticResource BoolToIndexConverter}}">
<ComboBoxItem Content="Real" />
<ComboBoxItem Content="Software" />
</ComboBox>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Margin="10,0,0,0"
Text="Software Keyboard" />
</Grid>
<!-- Browser Row -->
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox Width="100"
SelectedIndex="{Binding BrowserIsReal, Mode=TwoWay, Converter={StaticResource BoolToIndexConverter}}">
<ComboBoxItem Content="Real" />
<ComboBoxItem Content="Software" />
</ComboBox>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Margin="10,0,0,0"
Text="Browser" />
</Grid>
<!-- Controller Row -->
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox Width="100"
SelectedIndex="{Binding ControllerIsReal, Mode=TwoWay, Converter={StaticResource BoolToIndexConverter}}">
<ComboBoxItem Content="Real" />
<ComboBoxItem Content="Software" />
</ComboBox>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Margin="10,0,0,0"
Text="Controller" />
</Grid>
<!-- Player Select Row -->
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox Width="100"
SelectedIndex="{Binding PlayerSelectIsReal, Mode=TwoWay, Converter={StaticResource BoolToIndexConverter}}">
<ComboBoxItem Content="Real" />
<ComboBoxItem Content="Software" />
</ComboBox>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Margin="10,0,0,0"
Text="Player Select" />
</Grid>
<!-- Cabinet Row -->
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox Width="100"
SelectedIndex="{Binding CabinetIsReal, Mode=TwoWay, Converter={StaticResource BoolToIndexConverter}}">
<ComboBoxItem Content="Real" />
<ComboBoxItem Content="Software" />
</ComboBox>
<TextBlock Grid.Column="1"
VerticalAlignment="Center"
Margin="10,0,0,0"
Text="Cabinet" />
</Grid>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@ -0,0 +1,15 @@
using Avalonia.Controls;
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsRealAppsView : UserControl
{
public SettingsRealAppsView()
{
InitializeComponent();
}
}
}

View File

@ -38,6 +38,7 @@
<settings:SettingsAudioView Name="AudioPage" />
<settings:SettingsNetworkView Name="NetworkPage" />
<settings:SettingsLoggingView Name="LoggingPage" />
<settings:SettingsRealAppsView Name="RealAppletsPage" />
<settings:SettingsHacksView Name="HacksPage" />
</Grid>
<ui:NavigationView
@ -93,6 +94,10 @@
Content="{ext:Locale SettingsTabLogging}"
Tag="LoggingPage"
IconSource="Document" />
<ui:NavigationViewItem
Content="{ext:Locale SettingsTabRealApplets}"
Tag="RealAppletsPage"
IconSource="Document" />
<ui:NavigationViewItem
IsVisible="{Binding ShowDirtyHacks}"
Content="Dirty Hacks"

View File

@ -4,6 +4,7 @@ using Avalonia.Input;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Views.Settings;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Input;
using System;
@ -94,6 +95,9 @@ namespace Ryujinx.Ava.UI.Windows
case nameof(LoggingPage):
NavPanel.Content = LoggingPage;
break;
case nameof(RealAppletsPage):
NavPanel.Content = RealAppletsPage;
break;
case nameof(HacksPage):
HacksPage.DataContext = ViewModel;
NavPanel.Content = HacksPage;

View File

@ -439,6 +439,36 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// </summary>
public ulong[] DirtyHacks { get; set; }
/// <summary>
/// Runs missing applets as real ones
/// </summary>
public bool MissingAppletsAsReal { get; set; }
/// <summary>
/// Launches the SoftwareKeyboard as a real applet
/// </summary>
public bool SoftwareKeyboardIsReal { get; set; }
/// <summary>
/// Launches the Browser as a real applet
/// </summary>
public bool BrowserIsReal { get; set; }
/// <summary>
/// Launches the Controller as a real applet
/// </summary>
public bool ControllerIsReal { get; set; }
/// <summary>
/// Launches the PlayerSelect as a real applet
/// </summary>
public bool PlayerSelectIsReal { get; set; }
/// <summary>
/// Launches the Cabinet as a real applet
/// </summary>
public bool CabinetIsReal { get; set; }
/// <summary>
/// Loads a configuration file from disk
/// </summary>

View File

@ -100,6 +100,13 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.IgnoreApplet.Value = cff.IgnoreApplet;
System.UseHypervisor.Value = cff.UseHypervisor;
System.MissingAppletsAsReal.Value = cff.MissingAppletsAsReal;
System.SoftwareKeyboardIsReal.Value = cff.SoftwareKeyboardIsReal;
System.BrowserIsReal.Value = cff.BrowserIsReal;
System.ControllerIsReal.Value = cff.ControllerIsReal;
System.PlayerSelectIsReal.Value = cff.PlayerSelectIsReal;
System.CabinetIsReal.Value = cff.CabinetIsReal;
UI.GuiColumns.FavColumn.Value = cff.GuiColumns.FavColumn;
UI.GuiColumns.IconColumn.Value = cff.GuiColumns.IconColumn;
UI.GuiColumns.AppColumn.Value = cff.GuiColumns.AppColumn;
@ -416,6 +423,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
// This was accidentally enabled by default when it was PRed. That is not what we want,
// so as a compromise users who want to use it will simply need to re-enable it once after updating.
cff.IgnoreApplet = false;
cff.MissingAppletsAsReal = false;
}),
(60, static cff => cff.StartNoUI = false),
(61, static cff =>

View File

@ -383,6 +383,36 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// </summary>
public ReactiveObject<bool> UseHypervisor { get; private set; }
/// <summary>
/// Launches missing applets as real apps
/// </summary>
public ReactiveObject<bool> MissingAppletsAsReal { get; private set; }
/// <summary>
/// Launches the SoftwareKeyboard as a real applet
/// </summary>
public ReactiveObject<bool> SoftwareKeyboardIsReal { get; set; }
/// <summary>
/// Launches the Browser as a real applet
/// </summary>
public ReactiveObject<bool> BrowserIsReal { get; set; }
/// <summary>
/// Launches the Controller as a real applet
/// </summary>
public ReactiveObject<bool> ControllerIsReal { get; set; }
/// <summary>
/// Launches the PlayerSelect as a real applet
/// </summary>
public ReactiveObject<bool> PlayerSelectIsReal { get; set; }
/// <summary>
/// Launches the Cabinet as a real applet
/// </summary>
public ReactiveObject<bool> CabinetIsReal { get; set; }
public SystemSection()
{
Language = new ReactiveObject<Language>();
@ -423,6 +453,19 @@ namespace Ryujinx.Ava.Utilities.Configuration
AudioVolume.LogChangesToValue(nameof(AudioVolume));
UseHypervisor = new ReactiveObject<bool>();
UseHypervisor.LogChangesToValue(nameof(UseHypervisor));
MissingAppletsAsReal = new ReactiveObject<bool>();
MissingAppletsAsReal.LogChangesToValue(nameof(MissingAppletsAsReal));
SoftwareKeyboardIsReal = new ReactiveObject<bool>();
SoftwareKeyboardIsReal.LogChangesToValue(nameof(SoftwareKeyboardIsReal));
BrowserIsReal = new ReactiveObject<bool>();
BrowserIsReal.LogChangesToValue(nameof(BrowserIsReal));
ControllerIsReal = new ReactiveObject<bool>();
ControllerIsReal.LogChangesToValue(nameof(ControllerIsReal));
PlayerSelectIsReal = new ReactiveObject<bool>();
PlayerSelectIsReal.LogChangesToValue(nameof(PlayerSelectIsReal));
CabinetIsReal = new ReactiveObject<bool>();
CabinetIsReal.LogChangesToValue(nameof(CabinetIsReal));
}
}

View File

@ -141,6 +141,12 @@ namespace Ryujinx.Ava.Utilities.Configuration
LdnServer = Multiplayer.LdnServer,
ShowDirtyHacks = Hacks.ShowDirtyHacks,
DirtyHacks = Hacks.EnabledHacks.Select(it => it.Pack()).ToArray(),
MissingAppletsAsReal = System.MissingAppletsAsReal,
SoftwareKeyboardIsReal = System.SoftwareKeyboardIsReal,
BrowserIsReal = System.BrowserIsReal,
ControllerIsReal = System.ControllerIsReal,
PlayerSelectIsReal = System.PlayerSelectIsReal,
CabinetIsReal = System.CabinetIsReal,
};
return configurationFile;
@ -199,6 +205,12 @@ namespace Ryujinx.Ava.Utilities.Configuration
System.DramSize.Value = MemoryConfiguration.MemoryConfiguration4GiB;
System.IgnoreMissingServices.Value = false;
System.IgnoreApplet.Value = false;
System.MissingAppletsAsReal.Value = false;
System.SoftwareKeyboardIsReal.Value = false;
System.BrowserIsReal.Value = false;
System.ControllerIsReal.Value = false;
System.PlayerSelectIsReal.Value = false;
System.CabinetIsReal.Value = false;
System.UseHypervisor.Value = true;
Multiplayer.LanInterfaceId.Value = "0";
Multiplayer.Mode.Value = MultiplayerMode.Disabled;