Merge branch 'master' into aot
This commit is contained in:
commit
4316494ca0
20 changed files with 684 additions and 50 deletions
|
@ -12,7 +12,8 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
{
|
{
|
||||||
_appletMapping = new Dictionary<AppletId, Type>
|
_appletMapping = new Dictionary<AppletId, Type>
|
||||||
{
|
{
|
||||||
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) }
|
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) },
|
||||||
|
{ AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,9 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
{
|
{
|
||||||
event EventHandler AppletStateChanged;
|
event EventHandler AppletStateChanged;
|
||||||
|
|
||||||
ResultCode Start(AppletFifo<byte[]> inData, AppletFifo<byte[]> outData);
|
ResultCode Start(AppletSession normalSession,
|
||||||
|
AppletSession interactiveSession);
|
||||||
|
|
||||||
ResultCode GetResult();
|
ResultCode GetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
{
|
{
|
||||||
private Horizon _system;
|
private Horizon _system;
|
||||||
|
|
||||||
private AppletFifo<byte[]> _inputData;
|
private AppletSession _normalSession;
|
||||||
private AppletFifo<byte[]> _outputData;
|
private AppletSession _interactiveSession;
|
||||||
|
|
||||||
public event EventHandler AppletStateChanged;
|
public event EventHandler AppletStateChanged;
|
||||||
|
|
||||||
|
@ -19,13 +19,14 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
_system = system;
|
_system = system;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResultCode Start(AppletFifo<byte[]> inData, AppletFifo<byte[]> outData)
|
public ResultCode Start(AppletSession normalSession,
|
||||||
|
AppletSession interactiveSession)
|
||||||
{
|
{
|
||||||
_inputData = inData;
|
_normalSession = normalSession;
|
||||||
_outputData = outData;
|
_interactiveSession = interactiveSession;
|
||||||
|
|
||||||
// TODO(jduncanator): Parse PlayerSelectConfig from input data
|
// TODO(jduncanator): Parse PlayerSelectConfig from input data
|
||||||
_outputData.Push(BuildResponse());
|
_normalSession.Push(BuildResponse());
|
||||||
|
|
||||||
AppletStateChanged?.Invoke(this, null);
|
AppletStateChanged?.Invoke(this, null);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Applets
|
||||||
|
{
|
||||||
|
internal class SoftwareKeyboardApplet : IApplet
|
||||||
|
{
|
||||||
|
private const string DEFAULT_NUMB = "1";
|
||||||
|
private const string DEFAULT_TEXT = "Ryujinx";
|
||||||
|
|
||||||
|
private const int STANDARD_BUFFER_SIZE = 0x7D8;
|
||||||
|
private const int INTERACTIVE_BUFFER_SIZE = 0x7D4;
|
||||||
|
|
||||||
|
private SoftwareKeyboardState _state = SoftwareKeyboardState.Uninitialized;
|
||||||
|
|
||||||
|
private AppletSession _normalSession;
|
||||||
|
private AppletSession _interactiveSession;
|
||||||
|
|
||||||
|
private SoftwareKeyboardConfig _keyboardConfig;
|
||||||
|
|
||||||
|
private string _textValue = DEFAULT_TEXT;
|
||||||
|
|
||||||
|
public event EventHandler AppletStateChanged;
|
||||||
|
|
||||||
|
public SoftwareKeyboardApplet(Horizon system) { }
|
||||||
|
|
||||||
|
public ResultCode Start(AppletSession normalSession,
|
||||||
|
AppletSession interactiveSession)
|
||||||
|
{
|
||||||
|
_normalSession = normalSession;
|
||||||
|
_interactiveSession = interactiveSession;
|
||||||
|
|
||||||
|
_interactiveSession.DataAvailable += OnInteractiveData;
|
||||||
|
|
||||||
|
var launchParams = _normalSession.Pop();
|
||||||
|
var keyboardConfig = _normalSession.Pop();
|
||||||
|
var transferMemory = _normalSession.Pop();
|
||||||
|
|
||||||
|
_keyboardConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
|
||||||
|
|
||||||
|
_state = SoftwareKeyboardState.Ready;
|
||||||
|
|
||||||
|
Execute();
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResultCode GetResult()
|
||||||
|
{
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Execute()
|
||||||
|
{
|
||||||
|
// If the keyboard type is numbers only, we swap to a default
|
||||||
|
// text that only contains numbers.
|
||||||
|
if (_keyboardConfig.Type == SoftwareKeyboardType.NumbersOnly)
|
||||||
|
{
|
||||||
|
_textValue = DEFAULT_NUMB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the max string length is 0, we set it to a large default
|
||||||
|
// length.
|
||||||
|
if (_keyboardConfig.StringLengthMax == 0)
|
||||||
|
{
|
||||||
|
_keyboardConfig.StringLengthMax = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our default text is longer than the allowed length,
|
||||||
|
// we truncate it.
|
||||||
|
if (_textValue.Length > _keyboardConfig.StringLengthMax)
|
||||||
|
{
|
||||||
|
_textValue = _textValue.Substring(0, (int)_keyboardConfig.StringLengthMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_keyboardConfig.CheckText)
|
||||||
|
{
|
||||||
|
// If the application doesn't need to validate the response,
|
||||||
|
// we push the data to the non-interactive output buffer
|
||||||
|
// and poll it for completion.
|
||||||
|
_state = SoftwareKeyboardState.Complete;
|
||||||
|
|
||||||
|
_normalSession.Push(BuildResponse(_textValue, false));
|
||||||
|
|
||||||
|
AppletStateChanged?.Invoke(this, null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The application needs to validate the response, so we
|
||||||
|
// submit it to the interactive output buffer, and poll it
|
||||||
|
// for validation. Once validated, the application will submit
|
||||||
|
// back a validation status, which is handled in OnInteractiveDataPushIn.
|
||||||
|
_state = SoftwareKeyboardState.ValidationPending;
|
||||||
|
|
||||||
|
_interactiveSession.Push(BuildResponse(_textValue, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInteractiveData(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// Obtain the validation status response,
|
||||||
|
var data = _interactiveSession.Pop();
|
||||||
|
|
||||||
|
if (_state == SoftwareKeyboardState.ValidationPending)
|
||||||
|
{
|
||||||
|
// TODO(jduncantor):
|
||||||
|
// If application rejects our "attempt", submit another attempt,
|
||||||
|
// and put the applet back in PendingValidation state.
|
||||||
|
|
||||||
|
// For now we assume success, so we push the final result
|
||||||
|
// to the standard output buffer and carry on our merry way.
|
||||||
|
_normalSession.Push(BuildResponse(_textValue, false));
|
||||||
|
|
||||||
|
AppletStateChanged?.Invoke(this, null);
|
||||||
|
|
||||||
|
_state = SoftwareKeyboardState.Complete;
|
||||||
|
}
|
||||||
|
else if(_state == SoftwareKeyboardState.Complete)
|
||||||
|
{
|
||||||
|
// If we have already completed, we push the result text
|
||||||
|
// back on the output buffer and poll the application.
|
||||||
|
_normalSession.Push(BuildResponse(_textValue, false));
|
||||||
|
|
||||||
|
AppletStateChanged?.Invoke(this, null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We shouldn't be able to get here through standard swkbd execution.
|
||||||
|
throw new InvalidOperationException("Software Keyboard is in an invalid state.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] BuildResponse(string text, bool interactive)
|
||||||
|
{
|
||||||
|
int bufferSize = !interactive ? STANDARD_BUFFER_SIZE : INTERACTIVE_BUFFER_SIZE;
|
||||||
|
|
||||||
|
using (MemoryStream stream = new MemoryStream(new byte[bufferSize]))
|
||||||
|
using (BinaryWriter writer = new BinaryWriter(stream))
|
||||||
|
{
|
||||||
|
byte[] output = Encoding.Unicode.GetBytes(text);
|
||||||
|
|
||||||
|
if (!interactive)
|
||||||
|
{
|
||||||
|
// Result Code
|
||||||
|
writer.Write((uint)0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// In interactive mode, we write the length of the text
|
||||||
|
// as a long, rather than a result code.
|
||||||
|
writer.Write((long)output.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(output);
|
||||||
|
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T ReadStruct<T>(byte[] data)
|
||||||
|
where T : struct
|
||||||
|
{
|
||||||
|
GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
handle.Free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
|
||||||
|
{
|
||||||
|
// TODO(jduncanator): Define all fields
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
struct SoftwareKeyboardConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Type of keyboard.
|
||||||
|
/// </summary>
|
||||||
|
[FieldOffset(0x0)]
|
||||||
|
public SoftwareKeyboardType Type;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When non-zero, specifies the max string length. When the input is too long, swkbd will stop accepting more input until text is deleted via the B button (Backspace).
|
||||||
|
/// </summary>
|
||||||
|
[FieldOffset(0x3AC)]
|
||||||
|
public uint StringLengthMax;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When non-zero, specifies the max string length. When the input is too long, swkbd will display an icon and disable the ok-button.
|
||||||
|
/// </summary>
|
||||||
|
[FieldOffset(0x3B0)]
|
||||||
|
public uint StringLengthMaxExtended;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When set, the application will validate the entered text whilst the swkbd is still on screen.
|
||||||
|
/// </summary>
|
||||||
|
[FieldOffset(0x3D0), MarshalAs(UnmanagedType.I1)]
|
||||||
|
public bool CheckText;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
|
||||||
|
{
|
||||||
|
internal enum SoftwareKeyboardState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// swkbd is uninitialized.
|
||||||
|
/// </summary>
|
||||||
|
Uninitialized,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// swkbd is ready to process data.
|
||||||
|
/// </summary>
|
||||||
|
Ready,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// swkbd is awaiting an interactive reply with a validation status.
|
||||||
|
/// </summary>
|
||||||
|
ValidationPending,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// swkbd has completed.
|
||||||
|
/// </summary>
|
||||||
|
Complete
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
|
||||||
|
{
|
||||||
|
internal enum SoftwareKeyboardType : uint
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Normal keyboard.
|
||||||
|
/// </summary>
|
||||||
|
Default = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number pad. The buttons at the bottom left/right are only available when they're set in the config by leftButtonText / rightButtonText.
|
||||||
|
/// </summary>
|
||||||
|
NumbersOnly = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// QWERTY (and variants) keyboard only.
|
||||||
|
/// </summary>
|
||||||
|
LettersOnly = 2
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,35 +11,50 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
|
||||||
{
|
{
|
||||||
private IApplet _applet;
|
private IApplet _applet;
|
||||||
|
|
||||||
private AppletFifo<byte[]> _inData;
|
private AppletSession _normalSession;
|
||||||
private AppletFifo<byte[]> _outData;
|
private AppletSession _interactiveSession;
|
||||||
|
|
||||||
private KEvent _stateChangedEvent;
|
private KEvent _stateChangedEvent;
|
||||||
|
private KEvent _normalOutDataEvent;
|
||||||
|
private KEvent _interactiveOutDataEvent;
|
||||||
|
|
||||||
public ILibraryAppletAccessor(AppletId appletId, Horizon system)
|
public ILibraryAppletAccessor(AppletId appletId, Horizon system)
|
||||||
{
|
{
|
||||||
_stateChangedEvent = new KEvent(system);
|
_stateChangedEvent = new KEvent(system);
|
||||||
|
_normalOutDataEvent = new KEvent(system);
|
||||||
|
_interactiveOutDataEvent = new KEvent(system);
|
||||||
|
|
||||||
_applet = AppletManager.Create(appletId, system);
|
_applet = AppletManager.Create(appletId, system);
|
||||||
_inData = new AppletFifo<byte[]>();
|
|
||||||
_outData = new AppletFifo<byte[]>();
|
_normalSession = new AppletSession();
|
||||||
|
_interactiveSession = new AppletSession();
|
||||||
_applet.AppletStateChanged += OnAppletStateChanged;
|
|
||||||
|
_applet.AppletStateChanged += OnAppletStateChanged;
|
||||||
|
_normalSession.DataAvailable += OnNormalOutData;
|
||||||
|
_interactiveSession.DataAvailable += OnInteractiveOutData;
|
||||||
|
|
||||||
Logger.PrintInfo(LogClass.ServiceAm, $"Applet '{appletId}' created.");
|
Logger.PrintInfo(LogClass.ServiceAm, $"Applet '{appletId}' created.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAppletStateChanged(object sender, EventArgs e)
|
private void OnAppletStateChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_stateChangedEvent.ReadableEvent.Signal();
|
_stateChangedEvent.WritableEvent.Signal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNormalOutData(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_normalOutDataEvent.WritableEvent.Signal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInteractiveOutData(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_interactiveOutDataEvent.WritableEvent.Signal();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command(0)]
|
[Command(0)]
|
||||||
// GetAppletStateChangedEvent() -> handle<copy>
|
// GetAppletStateChangedEvent() -> handle<copy>
|
||||||
public ResultCode GetAppletStateChangedEvent(ServiceCtx context)
|
public ResultCode GetAppletStateChangedEvent(ServiceCtx context)
|
||||||
{
|
{
|
||||||
_stateChangedEvent.ReadableEvent.Signal();
|
|
||||||
|
|
||||||
if (context.Process.HandleTable.GenerateHandle(_stateChangedEvent.ReadableEvent, out int handle) != KernelResult.Success)
|
if (context.Process.HandleTable.GenerateHandle(_stateChangedEvent.ReadableEvent, out int handle) != KernelResult.Success)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Out of handles!");
|
throw new InvalidOperationException("Out of handles!");
|
||||||
|
@ -54,7 +69,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
|
||||||
// Start()
|
// Start()
|
||||||
public ResultCode Start(ServiceCtx context)
|
public ResultCode Start(ServiceCtx context)
|
||||||
{
|
{
|
||||||
return (ResultCode)_applet.Start(_inData, _outData);
|
return (ResultCode)_applet.Start(_normalSession.GetConsumer(),
|
||||||
|
_interactiveSession.GetConsumer());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command(30)]
|
[Command(30)]
|
||||||
|
@ -70,7 +86,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
|
||||||
{
|
{
|
||||||
IStorage data = GetObject<IStorage>(context, 0);
|
IStorage data = GetObject<IStorage>(context, 0);
|
||||||
|
|
||||||
_inData.Push(data.Data);
|
_normalSession.Push(data.Data);
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
@ -79,10 +95,70 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
|
||||||
// PopOutData() -> object<nn::am::service::IStorage>
|
// PopOutData() -> object<nn::am::service::IStorage>
|
||||||
public ResultCode PopOutData(ServiceCtx context)
|
public ResultCode PopOutData(ServiceCtx context)
|
||||||
{
|
{
|
||||||
byte[] data = _outData.Pop();
|
if(_normalSession.TryPop(out byte[] data))
|
||||||
|
{
|
||||||
|
MakeObject(context, new IStorage(data));
|
||||||
|
|
||||||
|
_normalOutDataEvent.WritableEvent.Clear();
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResultCode.NotAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(103)]
|
||||||
|
// PushInteractiveInData(object<nn::am::service::IStorage>)
|
||||||
|
public ResultCode PushInteractiveInData(ServiceCtx context)
|
||||||
|
{
|
||||||
|
IStorage data = GetObject<IStorage>(context, 0);
|
||||||
|
|
||||||
|
_interactiveSession.Push(data.Data);
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(104)]
|
||||||
|
// PopInteractiveOutData() -> object<nn::am::service::IStorage>
|
||||||
|
public ResultCode PopInteractiveOutData(ServiceCtx context)
|
||||||
|
{
|
||||||
|
if(_interactiveSession.TryPop(out byte[] data))
|
||||||
|
{
|
||||||
|
MakeObject(context, new IStorage(data));
|
||||||
|
|
||||||
|
_interactiveOutDataEvent.WritableEvent.Clear();
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResultCode.NotAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(105)]
|
||||||
|
// GetPopOutDataEvent() -> handle<copy>
|
||||||
|
public ResultCode GetPopOutDataEvent(ServiceCtx context)
|
||||||
|
{
|
||||||
|
if (context.Process.HandleTable.GenerateHandle(_normalOutDataEvent.ReadableEvent, out int handle) != KernelResult.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Out of handles!");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle);
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(106)]
|
||||||
|
// GetPopInteractiveOutDataEvent() -> handle<copy>
|
||||||
|
public ResultCode GetPopInteractiveOutDataEvent(ServiceCtx context)
|
||||||
|
{
|
||||||
|
if (context.Process.HandleTable.GenerateHandle(_interactiveOutDataEvent.ReadableEvent, out int handle) != KernelResult.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Out of handles!");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(handle);
|
||||||
|
|
||||||
MakeObject(context, new IStorage(data));
|
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,5 +29,19 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Command(11)]
|
||||||
|
// CreateTransferMemoryStorage(b8, u64, handle<copy>) -> object<nn::am::service::IStorage>
|
||||||
|
public ResultCode CreateTransferMemoryStorage(ServiceCtx context)
|
||||||
|
{
|
||||||
|
bool unknown = context.RequestData.ReadBoolean();
|
||||||
|
long size = context.RequestData.ReadInt64();
|
||||||
|
|
||||||
|
// NOTE: We don't support TransferMemory for now.
|
||||||
|
|
||||||
|
MakeObject(context, new IStorage(new byte[size]));
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,11 +5,26 @@ using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
{
|
{
|
||||||
internal class AppletFifo<T> : IEnumerable<T>
|
internal class AppletFifo<T> : IAppletFifo<T>
|
||||||
{
|
{
|
||||||
private ConcurrentQueue<T> _dataQueue;
|
private ConcurrentQueue<T> _dataQueue;
|
||||||
|
|
||||||
public int Count => _dataQueue.Count;
|
public event EventHandler DataAvailable;
|
||||||
|
|
||||||
|
public bool IsSynchronized
|
||||||
|
{
|
||||||
|
get { return ((ICollection)_dataQueue).IsSynchronized; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public object SyncRoot
|
||||||
|
{
|
||||||
|
get { return ((ICollection)_dataQueue).SyncRoot; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get { return _dataQueue.Count; }
|
||||||
|
}
|
||||||
|
|
||||||
public AppletFifo()
|
public AppletFifo()
|
||||||
{
|
{
|
||||||
|
@ -19,6 +34,22 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
public void Push(T item)
|
public void Push(T item)
|
||||||
{
|
{
|
||||||
_dataQueue.Enqueue(item);
|
_dataQueue.Enqueue(item);
|
||||||
|
|
||||||
|
DataAvailable?.Invoke(this, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryAdd(T item)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.Push(item);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public T Pop()
|
public T Pop()
|
||||||
|
@ -36,6 +67,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
return _dataQueue.TryDequeue(out result);
|
return _dataQueue.TryDequeue(out result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryTake(out T item)
|
||||||
|
{
|
||||||
|
return this.TryPop(out item);
|
||||||
|
}
|
||||||
|
|
||||||
public T Peek()
|
public T Peek()
|
||||||
{
|
{
|
||||||
if (_dataQueue.TryPeek(out T result))
|
if (_dataQueue.TryPeek(out T result))
|
||||||
|
@ -66,6 +102,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
_dataQueue.CopyTo(array, arrayIndex);
|
_dataQueue.CopyTo(array, arrayIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CopyTo(Array array, int index)
|
||||||
|
{
|
||||||
|
this.CopyTo((T[])array, index);
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerator<T> GetEnumerator()
|
public IEnumerator<T> GetEnumerator()
|
||||||
{
|
{
|
||||||
return _dataQueue.GetEnumerator();
|
return _dataQueue.GetEnumerator();
|
||||||
|
|
77
Ryujinx.HLE/HOS/Services/Am/AppletAE/AppletSession.cs
Normal file
77
Ryujinx.HLE/HOS/Services/Am/AppletAE/AppletSession.cs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
|
{
|
||||||
|
internal class AppletSession
|
||||||
|
{
|
||||||
|
private IAppletFifo<byte[]> _inputData;
|
||||||
|
private IAppletFifo<byte[]> _outputData;
|
||||||
|
|
||||||
|
public event EventHandler DataAvailable;
|
||||||
|
|
||||||
|
public int Length
|
||||||
|
{
|
||||||
|
get { return _inputData.Count; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppletSession()
|
||||||
|
: this(new AppletFifo<byte[]>(),
|
||||||
|
new AppletFifo<byte[]>())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public AppletSession(
|
||||||
|
IAppletFifo<byte[]> inputData,
|
||||||
|
IAppletFifo<byte[]> outputData)
|
||||||
|
{
|
||||||
|
_inputData = inputData;
|
||||||
|
_outputData = outputData;
|
||||||
|
|
||||||
|
_inputData.DataAvailable += OnDataAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataAvailable(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
DataAvailable?.Invoke(this, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Push(byte[] item)
|
||||||
|
{
|
||||||
|
if (!this.TryPush(item))
|
||||||
|
{
|
||||||
|
// TODO(jduncanator): Throw a proper exception
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryPush(byte[] item)
|
||||||
|
{
|
||||||
|
return _outputData.TryAdd(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Pop()
|
||||||
|
{
|
||||||
|
if (this.TryPop(out byte[] item))
|
||||||
|
{
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Input data empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryPop(out byte[] item)
|
||||||
|
{
|
||||||
|
return _inputData.TryTake(out item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This returns an AppletSession that can be used at the
|
||||||
|
/// other end of the pipe. Pushing data into this new session
|
||||||
|
/// will put it in the first session's input buffer, and vice
|
||||||
|
/// versa.
|
||||||
|
/// </summary>
|
||||||
|
public AppletSession GetConsumer()
|
||||||
|
{
|
||||||
|
return new AppletSession(this._outputData, this._inputData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
Ryujinx.HLE/HOS/Services/Am/AppletAE/IAppletFifo.cs
Normal file
10
Ryujinx.HLE/HOS/Services/Am/AppletAE/IAppletFifo.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
|
{
|
||||||
|
interface IAppletFifo<T> : IProducerConsumerCollection<T>
|
||||||
|
{
|
||||||
|
event EventHandler DataAvailable;
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,11 +24,17 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
// Write(u64, buffer<bytes, 0x21>)
|
// Write(u64, buffer<bytes, 0x21>)
|
||||||
public ResultCode Write(ServiceCtx context)
|
public ResultCode Write(ServiceCtx context)
|
||||||
{
|
{
|
||||||
// TODO: Error conditions.
|
|
||||||
long writePosition = context.RequestData.ReadInt64();
|
long writePosition = context.RequestData.ReadInt64();
|
||||||
|
|
||||||
|
if (writePosition > _storage.Data.Length)
|
||||||
|
{
|
||||||
|
return ResultCode.OutOfBounds;
|
||||||
|
}
|
||||||
|
|
||||||
(long position, long size) = context.Request.GetBufferType0x21();
|
(long position, long size) = context.Request.GetBufferType0x21();
|
||||||
|
|
||||||
|
size = Math.Min(size, _storage.Data.Length - writePosition);
|
||||||
|
|
||||||
if (size > 0)
|
if (size > 0)
|
||||||
{
|
{
|
||||||
long maxSize = _storage.Data.Length - writePosition;
|
long maxSize = _storage.Data.Length - writePosition;
|
||||||
|
@ -50,23 +56,20 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
// Read(u64) -> buffer<bytes, 0x22>
|
// Read(u64) -> buffer<bytes, 0x22>
|
||||||
public ResultCode Read(ServiceCtx context)
|
public ResultCode Read(ServiceCtx context)
|
||||||
{
|
{
|
||||||
// TODO: Error conditions.
|
|
||||||
long readPosition = context.RequestData.ReadInt64();
|
long readPosition = context.RequestData.ReadInt64();
|
||||||
|
|
||||||
|
if (readPosition > _storage.Data.Length)
|
||||||
|
{
|
||||||
|
return ResultCode.OutOfBounds;
|
||||||
|
}
|
||||||
|
|
||||||
(long position, long size) = context.Request.GetBufferType0x22();
|
(long position, long size) = context.Request.GetBufferType0x22();
|
||||||
|
|
||||||
byte[] data;
|
size = Math.Min(size, _storage.Data.Length - readPosition);
|
||||||
|
|
||||||
if (_storage.Data.Length > size)
|
byte[] data = new byte[size];
|
||||||
{
|
|
||||||
data = new byte[size];
|
|
||||||
|
|
||||||
Buffer.BlockCopy(_storage.Data, 0, data, 0, (int)size);
|
Buffer.BlockCopy(_storage.Data, (int)readPosition, data, 0, (int)size);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
data = _storage.Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Memory.WriteBytes(position, data);
|
context.Memory.WriteBytes(position, data);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ using Ryujinx.HLE.HOS.Kernel.Common;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService;
|
||||||
|
using Ryujinx.HLE.Utilities;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy
|
||||||
|
@ -31,13 +33,12 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||||
// EnsureSaveData(nn::account::Uid) -> u64
|
// EnsureSaveData(nn::account::Uid) -> u64
|
||||||
public ResultCode EnsureSaveData(ServiceCtx context)
|
public ResultCode EnsureSaveData(ServiceCtx context)
|
||||||
{
|
{
|
||||||
long uIdLow = context.RequestData.ReadInt64();
|
UInt128 userId = new UInt128(context.RequestData.ReadBytes(0x10));
|
||||||
long uIdHigh = context.RequestData.ReadInt64();
|
|
||||||
|
|
||||||
Logger.PrintStub(LogClass.ServiceAm);
|
|
||||||
|
|
||||||
context.ResponseData.Write(0L);
|
context.ResponseData.Write(0L);
|
||||||
|
|
||||||
|
Logger.PrintStub(LogClass.ServiceAm, new { userId });
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,9 +55,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||||
// SetTerminateResult(u32)
|
// SetTerminateResult(u32)
|
||||||
public ResultCode SetTerminateResult(ServiceCtx context)
|
public ResultCode SetTerminateResult(ServiceCtx context)
|
||||||
{
|
{
|
||||||
int errorCode = context.RequestData.ReadInt32();
|
int errorCode = context.RequestData.ReadInt32();
|
||||||
|
string result = GetFormattedErrorCode(errorCode);
|
||||||
string result = GetFormattedErrorCode(errorCode);
|
|
||||||
|
|
||||||
Logger.PrintInfo(LogClass.ServiceAm, $"Result = 0x{errorCode:x8} ({result}).");
|
Logger.PrintInfo(LogClass.ServiceAm, $"Result = 0x{errorCode:x8} ({result}).");
|
||||||
|
|
||||||
|
@ -95,10 +95,10 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||||
// GetPseudoDeviceId() -> nn::util::Uuid
|
// GetPseudoDeviceId() -> nn::util::Uuid
|
||||||
public ResultCode GetPseudoDeviceId(ServiceCtx context)
|
public ResultCode GetPseudoDeviceId(ServiceCtx context)
|
||||||
{
|
{
|
||||||
Logger.PrintStub(LogClass.ServiceAm);
|
context.ResponseData.Write(0L);
|
||||||
|
context.ResponseData.Write(0L);
|
||||||
|
|
||||||
context.ResponseData.Write(0L);
|
Logger.PrintStub(LogClass.ServiceAm);
|
||||||
context.ResponseData.Write(0L);
|
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
@ -118,11 +118,27 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||||
{
|
{
|
||||||
int state = context.RequestData.ReadInt32();
|
int state = context.RequestData.ReadInt32();
|
||||||
|
|
||||||
Logger.PrintStub(LogClass.ServiceAm);
|
Logger.PrintStub(LogClass.ServiceAm, new { state });
|
||||||
|
|
||||||
return ResultCode.Success;
|
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(130)] // 8.0.0+
|
[Command(130)] // 8.0.0+
|
||||||
// GetGpuErrorDetectedSystemEvent() -> handle<copy>
|
// GetGpuErrorDetectedSystemEvent() -> handle<copy>
|
||||||
public ResultCode GetGpuErrorDetectedSystemEvent(ServiceCtx context)
|
public ResultCode GetGpuErrorDetectedSystemEvent(ServiceCtx context)
|
||||||
|
|
|
@ -7,7 +7,10 @@ namespace Ryujinx.HLE.HOS.Services.Am
|
||||||
|
|
||||||
Success = 0,
|
Success = 0,
|
||||||
|
|
||||||
|
NotAvailable = (2 << ErrorCodeShift) | ModuleId,
|
||||||
NoMessages = (3 << ErrorCodeShift) | ModuleId,
|
NoMessages = (3 << ErrorCodeShift) | ModuleId,
|
||||||
|
ObjectInvalid = (500 << ErrorCodeShift) | ModuleId,
|
||||||
|
OutOfBounds = (503 << ErrorCodeShift) | ModuleId,
|
||||||
CpuBoostModeInvalid = (506 << ErrorCodeShift) | ModuleId
|
CpuBoostModeInvalid = (506 << ErrorCodeShift) | ModuleId
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,24 @@
|
||||||
namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm
|
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm
|
||||||
{
|
{
|
||||||
[Service("pdm:qry")]
|
[Service("pdm:qry")]
|
||||||
class IQueryService : IpcService
|
class IQueryService : IpcService
|
||||||
{
|
{
|
||||||
public IQueryService(ServiceCtx context) { }
|
public IQueryService(ServiceCtx context) { }
|
||||||
|
|
||||||
|
[Command(13)] // 5.0.0+
|
||||||
|
// QueryApplicationPlayStatisticsForSystem(buffer<bytes, 5> title_id_list) -> (buffer<bytes, 6> entries, s32 entries_count)
|
||||||
|
public ResultCode QueryApplicationPlayStatisticsForSystem(ServiceCtx context)
|
||||||
|
{
|
||||||
|
return QueryPlayStatisticsManager.GetPlayStatistics(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command(16)] // 6.0.0+
|
||||||
|
// QueryApplicationPlayStatisticsByUserAccountIdForSystem(nn::account::Uid, buffer<bytes, 5> title_id_list) -> (buffer<bytes, 6> entries, s32 entries_count)
|
||||||
|
public ResultCode QueryApplicationPlayStatisticsByUserAccountIdForSystem(ServiceCtx context)
|
||||||
|
{
|
||||||
|
return QueryPlayStatisticsManager.GetPlayStatistics(context, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
using ARMeilleure.Memory;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types;
|
||||||
|
using Ryujinx.HLE.Utilities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
|
||||||
|
{
|
||||||
|
static class QueryPlayStatisticsManager
|
||||||
|
{
|
||||||
|
private static Dictionary<UInt128, ApplicationPlayStatistics> applicationPlayStatistics = new Dictionary<UInt128, ApplicationPlayStatistics>();
|
||||||
|
|
||||||
|
internal static ResultCode GetPlayStatistics(ServiceCtx context, bool byUserId = false)
|
||||||
|
{
|
||||||
|
long inputPosition = context.Request.SendBuff[0].Position;
|
||||||
|
long inputSize = context.Request.SendBuff[0].Size;
|
||||||
|
|
||||||
|
long outputPosition = context.Request.ReceiveBuff[0].Position;
|
||||||
|
long outputSize = context.Request.ReceiveBuff[0].Size;
|
||||||
|
|
||||||
|
UInt128 userId = byUserId ? new UInt128(context.RequestData.ReadBytes(0x10)) : new UInt128();
|
||||||
|
|
||||||
|
if (byUserId)
|
||||||
|
{
|
||||||
|
if (!context.Device.System.State.Account.TryGetUser(userId, out _))
|
||||||
|
{
|
||||||
|
return ResultCode.UserNotFound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.System.ControlData.PlayLogQueryCapability;
|
||||||
|
|
||||||
|
List<ulong> titleIds = new List<ulong>();
|
||||||
|
|
||||||
|
for (int i = 0; i < inputSize / sizeof(ulong); i++)
|
||||||
|
{
|
||||||
|
titleIds.Add(BitConverter.ToUInt64(context.Memory.ReadBytes(inputPosition, inputSize), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryCapability == PlayLogQueryCapability.WhiteList)
|
||||||
|
{
|
||||||
|
// Check if input title ids are in the whitelist.
|
||||||
|
foreach (ulong titleId in titleIds)
|
||||||
|
{
|
||||||
|
if (!context.Device.System.ControlData.PlayLogQueryableApplicationId.Contains(titleId))
|
||||||
|
{
|
||||||
|
return (ResultCode)Am.ResultCode.ObjectInvalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize);
|
||||||
|
|
||||||
|
// Return ResultCode.ServiceUnavailable if data is locked by another process.
|
||||||
|
var filteredApplicationPlayStatistics = applicationPlayStatistics.AsEnumerable();
|
||||||
|
|
||||||
|
if (queryCapability == PlayLogQueryCapability.None)
|
||||||
|
{
|
||||||
|
filteredApplicationPlayStatistics = filteredApplicationPlayStatistics.Where(kv => kv.Value.TitleId == context.Process.TitleId);
|
||||||
|
}
|
||||||
|
else // PlayLogQueryCapability.All
|
||||||
|
{
|
||||||
|
filteredApplicationPlayStatistics = filteredApplicationPlayStatistics.Where(kv => titleIds.Contains(kv.Value.TitleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byUserId)
|
||||||
|
{
|
||||||
|
filteredApplicationPlayStatistics = filteredApplicationPlayStatistics.Where(kv => kv.Key == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < filteredApplicationPlayStatistics.Count(); i++)
|
||||||
|
{
|
||||||
|
MemoryHelper.Write(context.Memory, outputPosition + (i * Marshal.SizeOf<ApplicationPlayStatistics>()), filteredApplicationPlayStatistics.ElementAt(i).Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.ResponseData.Write(filteredApplicationPlayStatistics.Count());
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types
|
||||||
|
{
|
||||||
|
[StructLayout(LayoutKind.Sequential, Size = 0x18)]
|
||||||
|
struct ApplicationPlayStatistics
|
||||||
|
{
|
||||||
|
public ulong TitleId;
|
||||||
|
public long TotalPlayTime; // In nanoseconds.
|
||||||
|
public long TotalLaunchCount;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types
|
||||||
|
{
|
||||||
|
enum PlayLogQueryCapability
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
WhiteList,
|
||||||
|
All
|
||||||
|
}
|
||||||
|
}
|
13
Ryujinx.HLE/HOS/Services/Sdb/Pdm/ResultCode.cs
Normal file
13
Ryujinx.HLE/HOS/Services/Sdb/Pdm/ResultCode.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm
|
||||||
|
{
|
||||||
|
enum ResultCode
|
||||||
|
{
|
||||||
|
ModuleId = 178,
|
||||||
|
ErrorCodeShift = 9,
|
||||||
|
|
||||||
|
Success = 0,
|
||||||
|
|
||||||
|
UserNotFound = (101 << ErrorCodeShift) | ModuleId,
|
||||||
|
ServiceUnavailable = (150 << ErrorCodeShift) | ModuleId
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue