33f8284bc0
* hos/gui: Add a check of NCA program index in titleid This add a check to `ApplicationLoader` for the last 2 digits of the game TitleId who seems to be the NCA program index. We currently return the last index, instead of the lower one. Same check is added to ApplicationLibrary in the UI. I've cleaned up both file too. * hle: implement partial relaunch logic TODO: make the emulator auto relauch. * Handle auto relaunch * hle: Unify update usage system * hle: Implement support of multi programs in update system * Add some documentation * Address rip's comment Co-authored-by: Ac_K <Acoustik666@gmail.com>
494 lines
19 KiB
C#
494 lines
19 KiB
C#
using LibHac;
|
|
using LibHac.Account;
|
|
using LibHac.Common;
|
|
using LibHac.Fs;
|
|
using LibHac.Ns;
|
|
using Ryujinx.Common;
|
|
using Ryujinx.Common.Logging;
|
|
using Ryujinx.HLE.Exceptions;
|
|
using Ryujinx.HLE.HOS.Ipc;
|
|
using Ryujinx.HLE.HOS.Kernel.Common;
|
|
using Ryujinx.HLE.HOS.Kernel.Memory;
|
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
|
|
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
|
|
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService;
|
|
using Ryujinx.HLE.HOS.SystemState;
|
|
using System;
|
|
using System.Numerics;
|
|
|
|
using static LibHac.Fs.ApplicationSaveDataManagement;
|
|
using AccountUid = Ryujinx.HLE.HOS.Services.Account.Acc.UserId;
|
|
using ApplicationId = LibHac.Ncm.ApplicationId;
|
|
|
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy
|
|
{
|
|
class IApplicationFunctions : IpcService
|
|
{
|
|
private KEvent _gpuErrorDetectedSystemEvent;
|
|
private KEvent _friendInvitationStorageChannelEvent;
|
|
private KEvent _notificationStorageChannelEvent;
|
|
|
|
public IApplicationFunctions(Horizon system)
|
|
{
|
|
_gpuErrorDetectedSystemEvent = new KEvent(system.KernelContext);
|
|
_friendInvitationStorageChannelEvent = new KEvent(system.KernelContext);
|
|
_notificationStorageChannelEvent = new KEvent(system.KernelContext);
|
|
}
|
|
|
|
[Command(1)]
|
|
// PopLaunchParameter(LaunchParameterKind kind) -> object<nn::am::service::IStorage>
|
|
public ResultCode PopLaunchParameter(ServiceCtx context)
|
|
{
|
|
LaunchParameterKind kind = (LaunchParameterKind)context.RequestData.ReadUInt32();
|
|
|
|
byte[] storageData;
|
|
|
|
switch (kind)
|
|
{
|
|
case LaunchParameterKind.UserChannel:
|
|
storageData = context.Device.UserChannelPersistence.Pop();
|
|
break;
|
|
case LaunchParameterKind.PreselectedUser:
|
|
// Only the first 0x18 bytes of the Data seems to be actually used.
|
|
storageData = StorageHelper.MakeLaunchParams(context.Device.System.State.Account.LastOpenedUser);
|
|
break;
|
|
default:
|
|
throw new NotImplementedException($"Unknown LaunchParameterKind {kind}");
|
|
}
|
|
|
|
MakeObject(context, new AppletAE.IStorage(storageData));
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(20)]
|
|
// EnsureSaveData(nn::account::Uid) -> u64
|
|
public ResultCode EnsureSaveData(ServiceCtx context)
|
|
{
|
|
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
|
|
ApplicationId applicationId = new ApplicationId(context.Process.TitleId);
|
|
|
|
BlitStruct<ApplicationControlProperty> controlHolder = context.Device.Application.ControlData;
|
|
|
|
ref ApplicationControlProperty control = ref controlHolder.Value;
|
|
|
|
if (LibHac.Utilities.IsEmpty(controlHolder.ByteSpan))
|
|
{
|
|
// If the current application doesn't have a loaded control property, create a dummy one
|
|
// and set the savedata sizes so a user savedata will be created.
|
|
control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
|
|
|
|
// The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
|
|
control.UserAccountSaveDataSize = 0x4000;
|
|
control.UserAccountSaveDataJournalSize = 0x4000;
|
|
|
|
Logger.Warning?.Print(LogClass.ServiceAm,
|
|
"No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
|
|
}
|
|
|
|
Result result = EnsureApplicationSaveData(context.Device.FileSystem.FsClient, out long requiredSize, applicationId,
|
|
ref control, ref userId);
|
|
|
|
context.ResponseData.Write(requiredSize);
|
|
|
|
return (ResultCode)result.Value;
|
|
}
|
|
|
|
[Command(21)]
|
|
// GetDesiredLanguage() -> nn::settings::LanguageCode
|
|
public ResultCode GetDesiredLanguage(ServiceCtx context)
|
|
{
|
|
// This seems to be calling ns:am GetApplicationDesiredLanguage followed by ConvertApplicationLanguageToLanguageCode
|
|
// Calls are from a IReadOnlyApplicationControlDataInterface object
|
|
// ConvertApplicationLanguageToLanguageCode compares language code strings and returns the index
|
|
// TODO: When above calls are implemented, switch to using ns:am
|
|
|
|
long desiredLanguageCode = context.Device.System.State.DesiredLanguageCode;
|
|
|
|
int supportedLanguages = (int)context.Device.Application.ControlData.Value.SupportedLanguages;
|
|
int firstSupported = BitOperations.TrailingZeroCount(supportedLanguages);
|
|
|
|
if (firstSupported > (int)SystemState.TitleLanguage.Chinese)
|
|
{
|
|
Logger.Warning?.Print(LogClass.ServiceAm, "Application has zero supported languages");
|
|
|
|
context.ResponseData.Write(desiredLanguageCode);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
// If desired language is not supported by application, use first supported language from TitleLanguage.
|
|
// TODO: In the future, a GUI could enable user-specified search priority
|
|
if (((1 << (int)context.Device.System.State.DesiredTitleLanguage) & supportedLanguages) == 0)
|
|
{
|
|
SystemLanguage newLanguage = Enum.Parse<SystemLanguage>(Enum.GetName(typeof(SystemState.TitleLanguage), firstSupported));
|
|
desiredLanguageCode = SystemStateMgr.GetLanguageCode((int)newLanguage);
|
|
|
|
Logger.Info?.Print(LogClass.ServiceAm, $"Application doesn't support configured language. Using {newLanguage}");
|
|
}
|
|
|
|
context.ResponseData.Write(desiredLanguageCode);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(22)]
|
|
// SetTerminateResult(u32)
|
|
public ResultCode SetTerminateResult(ServiceCtx context)
|
|
{
|
|
Result result = new Result(context.RequestData.ReadUInt32());
|
|
|
|
Logger.Info?.Print(LogClass.ServiceAm, $"Result = 0x{result.Value:x8} ({result.ToStringWithName()}).");
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(23)]
|
|
// GetDisplayVersion() -> nn::oe::DisplayVersion
|
|
public ResultCode GetDisplayVersion(ServiceCtx context)
|
|
{
|
|
// This should work as DisplayVersion U8Span always gives a 0x10 size byte array.
|
|
// If an NACP isn't found, the buffer will be all '\0' which seems to be the correct implementation.
|
|
context.ResponseData.Write(context.Device.Application.ControlData.Value.DisplayVersion);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
// GetSaveDataSize(u8, nn::account::Uid) -> (u64, u64)
|
|
[Command(26)] // 3.0.0+
|
|
public ResultCode GetSaveDataSize(ServiceCtx context)
|
|
{
|
|
SaveDataType saveDataType = (SaveDataType)context.RequestData.ReadByte();
|
|
context.RequestData.BaseStream.Seek(7, System.IO.SeekOrigin.Current);
|
|
|
|
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
|
|
|
|
// TODO: We return a size of 2GB as we use a directory based save system. This should be enough for most of the games.
|
|
context.ResponseData.Write(2000000000u);
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { saveDataType, userId });
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(30)]
|
|
// BeginBlockingHomeButtonShortAndLongPressed()
|
|
public ResultCode BeginBlockingHomeButtonShortAndLongPressed(ServiceCtx context)
|
|
{
|
|
// NOTE: This set two internal fields at offsets 0x89 and 0x8B to value 1 then it signals an internal event.
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(31)]
|
|
// EndBlockingHomeButtonShortAndLongPressed()
|
|
public ResultCode EndBlockingHomeButtonShortAndLongPressed(ServiceCtx context)
|
|
{
|
|
// NOTE: This set two internal fields at offsets 0x89 and 0x8B to value 0 then it signals an internal event.
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(32)] // 2.0.0+
|
|
// BeginBlockingHomeButton(u64 nano_second)
|
|
public ResultCode BeginBlockingHomeButton(ServiceCtx context)
|
|
{
|
|
ulong nanoSeconds = context.RequestData.ReadUInt64();
|
|
|
|
// NOTE: This set two internal fields at offsets 0x89 to value 1 and 0x90 to value of "nanoSeconds" then it signals an internal event.
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { nanoSeconds });
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(33)] // 2.0.0+
|
|
// EndBlockingHomeButton()
|
|
public ResultCode EndBlockingHomeButton(ServiceCtx context)
|
|
{
|
|
// NOTE: This set two internal fields at offsets 0x89 and 0x90 to value 0 then it signals an internal event.
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(40)]
|
|
// NotifyRunning() -> b8
|
|
public ResultCode NotifyRunning(ServiceCtx context)
|
|
{
|
|
context.ResponseData.Write(true);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(50)] // 2.0.0+
|
|
// GetPseudoDeviceId() -> nn::util::Uuid
|
|
public ResultCode GetPseudoDeviceId(ServiceCtx context)
|
|
{
|
|
context.ResponseData.Write(0L);
|
|
context.ResponseData.Write(0L);
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(66)] // 3.0.0+
|
|
// InitializeGamePlayRecording(u64, handle<copy>)
|
|
public ResultCode InitializeGamePlayRecording(ServiceCtx context)
|
|
{
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(67)] // 3.0.0+
|
|
// SetGamePlayRecordingState(u32)
|
|
public ResultCode SetGamePlayRecordingState(ServiceCtx context)
|
|
{
|
|
int state = context.RequestData.ReadInt32();
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { state });
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(90)] // 4.0.0+
|
|
// EnableApplicationCrashReport(u8)
|
|
public ResultCode EnableApplicationCrashReport(ServiceCtx context)
|
|
{
|
|
bool applicationCrashReportEnabled = context.RequestData.ReadBoolean();
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { applicationCrashReportEnabled });
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(100)] // 5.0.0+
|
|
// InitializeApplicationCopyrightFrameBuffer(s32 width, s32 height, handle<copy, transfer_memory> transfer_memory, u64 transfer_memory_size)
|
|
public ResultCode InitializeApplicationCopyrightFrameBuffer(ServiceCtx context)
|
|
{
|
|
int width = context.RequestData.ReadInt32();
|
|
int height = context.RequestData.ReadInt32();
|
|
ulong transferMemorySize = context.RequestData.ReadUInt64();
|
|
int transferMemoryHandle = context.Request.HandleDesc.ToCopy[0];
|
|
ulong transferMemoryAddress = context.Process.HandleTable.GetObject<KTransferMemory>(transferMemoryHandle).Address;
|
|
|
|
ResultCode resultCode = ResultCode.InvalidParameters;
|
|
|
|
if (((transferMemorySize & 0x3FFFF) == 0) && width <= 1280 && height <= 720)
|
|
{
|
|
resultCode = InitializeApplicationCopyrightFrameBufferImpl(transferMemoryAddress, transferMemorySize, width, height);
|
|
}
|
|
|
|
/*
|
|
if (transferMemoryHandle)
|
|
{
|
|
svcCloseHandle(transferMemoryHandle);
|
|
}
|
|
*/
|
|
|
|
return resultCode;
|
|
}
|
|
|
|
private ResultCode InitializeApplicationCopyrightFrameBufferImpl(ulong transferMemoryAddress, ulong transferMemorySize, int width, int height)
|
|
{
|
|
ResultCode resultCode = ResultCode.ObjectInvalid;
|
|
|
|
if ((transferMemorySize & 0x3FFFF) != 0)
|
|
{
|
|
return ResultCode.InvalidParameters;
|
|
}
|
|
|
|
// if (_copyrightBuffer == null)
|
|
{
|
|
// TODO: Initialize buffer and object.
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { transferMemoryAddress, transferMemorySize, width, height });
|
|
|
|
resultCode = ResultCode.Success;
|
|
}
|
|
|
|
return resultCode;
|
|
}
|
|
|
|
[Command(101)] // 5.0.0+
|
|
// SetApplicationCopyrightImage(buffer<bytes, 0x45> frame_buffer, s32 x, s32 y, s32 width, s32 height, s32 window_origin_mode)
|
|
public ResultCode SetApplicationCopyrightImage(ServiceCtx context)
|
|
{
|
|
long frameBufferPos = context.Request.SendBuff[0].Position;
|
|
long frameBufferSize = context.Request.SendBuff[0].Size;
|
|
int x = context.RequestData.ReadInt32();
|
|
int y = context.RequestData.ReadInt32();
|
|
int width = context.RequestData.ReadInt32();
|
|
int height = context.RequestData.ReadInt32();
|
|
uint windowOriginMode = context.RequestData.ReadUInt32();
|
|
|
|
ResultCode resultCode = ResultCode.InvalidParameters;
|
|
|
|
if (((y | x) >= 0) && width >= 1 && height >= 1)
|
|
{
|
|
ResultCode result = SetApplicationCopyrightImageImpl(x, y, width, height, frameBufferPos, frameBufferSize, windowOriginMode);
|
|
|
|
if (result != ResultCode.Success)
|
|
{
|
|
resultCode = result;
|
|
}
|
|
else
|
|
{
|
|
resultCode = ResultCode.Success;
|
|
}
|
|
}
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { frameBufferPos, frameBufferSize, x, y, width, height, windowOriginMode });
|
|
|
|
return resultCode;
|
|
}
|
|
|
|
private ResultCode SetApplicationCopyrightImageImpl(int x, int y, int width, int height, long frameBufferPos, long frameBufferSize, uint windowOriginMode)
|
|
{
|
|
/*
|
|
if (_copyrightBuffer == null)
|
|
{
|
|
return ResultCode.NullCopyrightObject;
|
|
}
|
|
*/
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { x, y, width, height, frameBufferPos, frameBufferSize, windowOriginMode });
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(102)] // 5.0.0+
|
|
// SetApplicationCopyrightVisibility(bool visible)
|
|
public ResultCode SetApplicationCopyrightVisibility(ServiceCtx context)
|
|
{
|
|
bool visible = context.RequestData.ReadBoolean();
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { visible });
|
|
|
|
// NOTE: It sets an internal field and return ResultCode.Success in all case.
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(110)] // 5.0.0+
|
|
// QueryApplicationPlayStatistics(buffer<bytes, 5> title_id_list) -> (buffer<bytes, 6> entries, s32 entries_count)
|
|
public ResultCode QueryApplicationPlayStatistics(ServiceCtx context)
|
|
{
|
|
// TODO: Call pdm:qry cmd 13 when IPC call between services will be implemented.
|
|
return (ResultCode)QueryPlayStatisticsManager.GetPlayStatistics(context);
|
|
}
|
|
|
|
[Command(111)] // 6.0.0+
|
|
// QueryApplicationPlayStatisticsByUid(nn::account::Uid, buffer<bytes, 5> title_id_list) -> (buffer<bytes, 6> entries, s32 entries_count)
|
|
public ResultCode QueryApplicationPlayStatisticsByUid(ServiceCtx context)
|
|
{
|
|
// TODO: Call pdm:qry cmd 16 when IPC call between services will be implemented.
|
|
return (ResultCode)QueryPlayStatisticsManager.GetPlayStatistics(context, true);
|
|
}
|
|
|
|
[Command(120)] // 5.0.0+
|
|
// ExecuteProgram(ProgramSpecifyKind kind, u64 value)
|
|
public ResultCode ExecuteProgram(ServiceCtx context)
|
|
{
|
|
ProgramSpecifyKind kind = (ProgramSpecifyKind)context.RequestData.ReadUInt32();
|
|
|
|
// padding
|
|
context.RequestData.ReadUInt32();
|
|
|
|
ulong value = context.RequestData.ReadUInt64();
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { kind, value });
|
|
|
|
context.Device.UiHandler.ExecuteProgram(context.Device, kind, value);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(121)] // 5.0.0+
|
|
// ClearUserChannel()
|
|
public ResultCode ClearUserChannel(ServiceCtx context)
|
|
{
|
|
context.Device.UserChannelPersistence.Clear();
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(122)] // 5.0.0+
|
|
// UnpopToUserChannel(object<nn::am::service::IStorage> input_storage)
|
|
public ResultCode UnpopToUserChannel(ServiceCtx context)
|
|
{
|
|
AppletAE.IStorage data = GetObject<AppletAE.IStorage>(context, 0);
|
|
|
|
context.Device.UserChannelPersistence.Push(data.Data);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(123)] // 5.0.0+
|
|
// GetPreviousProgramIndex() -> s32 program_index
|
|
public ResultCode GetPreviousProgramIndex(ServiceCtx context)
|
|
{
|
|
int previousProgramIndex = context.Device.UserChannelPersistence.PreviousIndex;
|
|
|
|
context.ResponseData.Write(previousProgramIndex);
|
|
|
|
Logger.Stub?.PrintStub(LogClass.ServiceAm, new { previousProgramIndex });
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(130)] // 8.0.0+
|
|
// GetGpuErrorDetectedSystemEvent() -> handle<copy>
|
|
public ResultCode GetGpuErrorDetectedSystemEvent(ServiceCtx context)
|
|
{
|
|
if (context.Process.HandleTable.GenerateHandle(_gpuErrorDetectedSystemEvent.ReadableEvent, out int gpuErrorDetectedSystemEventHandle) != KernelResult.Success)
|
|
{
|
|
throw new InvalidOperationException("Out of handles!");
|
|
}
|
|
|
|
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(gpuErrorDetectedSystemEventHandle);
|
|
|
|
// NOTE: This is used by "sdk" NSO during applet-application initialization.
|
|
// A seperate thread is setup where event-waiting is handled.
|
|
// When the Event is signaled, official sw will assert.
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(140)] // 9.0.0+
|
|
// GetFriendInvitationStorageChannelEvent() -> handle<copy>
|
|
public ResultCode GetFriendInvitationStorageChannelEvent(ServiceCtx context)
|
|
{
|
|
if (context.Process.HandleTable.GenerateHandle(_friendInvitationStorageChannelEvent.ReadableEvent, out int friendInvitationStorageChannelEventHandle) != KernelResult.Success)
|
|
{
|
|
throw new InvalidOperationException("Out of handles!");
|
|
}
|
|
|
|
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(friendInvitationStorageChannelEventHandle);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
|
|
[Command(150)] // 9.0.0+
|
|
// GetNotificationStorageChannelEvent() -> handle<copy>
|
|
public ResultCode GetNotificationStorageChannelEvent(ServiceCtx context)
|
|
{
|
|
if (context.Process.HandleTable.GenerateHandle(_notificationStorageChannelEvent.ReadableEvent, out int notificationStorageChannelEventHandle) != KernelResult.Success)
|
|
{
|
|
throw new InvalidOperationException("Out of handles!");
|
|
}
|
|
|
|
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(notificationStorageChannelEventHandle);
|
|
|
|
return ResultCode.Success;
|
|
}
|
|
}
|
|
} |