Merge branch 'master' into aot

This commit is contained in:
LDj3SNuD 2019-11-21 03:11:48 +01:00
commit 4316494ca0
20 changed files with 684 additions and 50 deletions

View file

@ -12,7 +12,8 @@ namespace Ryujinx.HLE.HOS.Applets
{
_appletMapping = new Dictionary<AppletId, Type>
{
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) }
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) },
{ AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) }
};
}

View file

@ -7,7 +7,9 @@ namespace Ryujinx.HLE.HOS.Applets
{
event EventHandler AppletStateChanged;
ResultCode Start(AppletFifo<byte[]> inData, AppletFifo<byte[]> outData);
ResultCode Start(AppletSession normalSession,
AppletSession interactiveSession);
ResultCode GetResult();
}
}

View file

@ -9,8 +9,8 @@ namespace Ryujinx.HLE.HOS.Applets
{
private Horizon _system;
private AppletFifo<byte[]> _inputData;
private AppletFifo<byte[]> _outputData;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
public event EventHandler AppletStateChanged;
@ -19,13 +19,14 @@ namespace Ryujinx.HLE.HOS.Applets
_system = system;
}
public ResultCode Start(AppletFifo<byte[]> inData, AppletFifo<byte[]> outData)
public ResultCode Start(AppletSession normalSession,
AppletSession interactiveSession)
{
_inputData = inData;
_outputData = outData;
_normalSession = normalSession;
_interactiveSession = interactiveSession;
// TODO(jduncanator): Parse PlayerSelectConfig from input data
_outputData.Push(BuildResponse());
_normalSession.Push(BuildResponse());
AppletStateChanged?.Invoke(this, null);

View file

@ -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();
}
}
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -11,35 +11,50 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
{
private IApplet _applet;
private AppletFifo<byte[]> _inData;
private AppletFifo<byte[]> _outData;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
private KEvent _stateChangedEvent;
private KEvent _normalOutDataEvent;
private KEvent _interactiveOutDataEvent;
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);
_inData = new AppletFifo<byte[]>();
_outData = new AppletFifo<byte[]>();
_applet.AppletStateChanged += OnAppletStateChanged;
_applet = AppletManager.Create(appletId, system);
_normalSession = new AppletSession();
_interactiveSession = new AppletSession();
_applet.AppletStateChanged += OnAppletStateChanged;
_normalSession.DataAvailable += OnNormalOutData;
_interactiveSession.DataAvailable += OnInteractiveOutData;
Logger.PrintInfo(LogClass.ServiceAm, $"Applet '{appletId}' created.");
}
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)]
// GetAppletStateChangedEvent() -> handle<copy>
public ResultCode GetAppletStateChangedEvent(ServiceCtx context)
{
_stateChangedEvent.ReadableEvent.Signal();
if (context.Process.HandleTable.GenerateHandle(_stateChangedEvent.ReadableEvent, out int handle) != KernelResult.Success)
{
throw new InvalidOperationException("Out of handles!");
@ -54,7 +69,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
// Start()
public ResultCode Start(ServiceCtx context)
{
return (ResultCode)_applet.Start(_inData, _outData);
return (ResultCode)_applet.Start(_normalSession.GetConsumer(),
_interactiveSession.GetConsumer());
}
[Command(30)]
@ -70,7 +86,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
{
IStorage data = GetObject<IStorage>(context, 0);
_inData.Push(data.Data);
_normalSession.Push(data.Data);
return ResultCode.Success;
}
@ -79,10 +95,70 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
// PopOutData() -> object<nn::am::service::IStorage>
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;
}
}

View file

@ -29,5 +29,19 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
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;
}
}
}

View file

@ -5,11 +5,26 @@ using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
{
internal class AppletFifo<T> : IEnumerable<T>
internal class AppletFifo<T> : IAppletFifo<T>
{
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()
{
@ -19,6 +34,22 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
public void Push(T 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()
@ -36,6 +67,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
return _dataQueue.TryDequeue(out result);
}
public bool TryTake(out T item)
{
return this.TryPop(out item);
}
public T Peek()
{
if (_dataQueue.TryPeek(out T result))
@ -66,6 +102,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
_dataQueue.CopyTo(array, arrayIndex);
}
public void CopyTo(Array array, int index)
{
this.CopyTo((T[])array, index);
}
public IEnumerator<T> GetEnumerator()
{
return _dataQueue.GetEnumerator();

View 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);
}
}
}

View 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;
}
}

View file

@ -24,11 +24,17 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
// Write(u64, buffer<bytes, 0x21>)
public ResultCode Write(ServiceCtx context)
{
// TODO: Error conditions.
long writePosition = context.RequestData.ReadInt64();
if (writePosition > _storage.Data.Length)
{
return ResultCode.OutOfBounds;
}
(long position, long size) = context.Request.GetBufferType0x21();
size = Math.Min(size, _storage.Data.Length - writePosition);
if (size > 0)
{
long maxSize = _storage.Data.Length - writePosition;
@ -50,23 +56,20 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
// Read(u64) -> buffer<bytes, 0x22>
public ResultCode Read(ServiceCtx context)
{
// TODO: Error conditions.
long readPosition = context.RequestData.ReadInt64();
if (readPosition > _storage.Data.Length)
{
return ResultCode.OutOfBounds;
}
(long position, long size) = context.Request.GetBufferType0x22();
byte[] data;
size = Math.Min(size, _storage.Data.Length - readPosition);
if (_storage.Data.Length > size)
{
data = new byte[size];
byte[] data = new byte[size];
Buffer.BlockCopy(_storage.Data, 0, data, 0, (int)size);
}
else
{
data = _storage.Data;
}
Buffer.BlockCopy(_storage.Data, (int)readPosition, data, 0, (int)size);
context.Memory.WriteBytes(position, data);

View file

@ -4,6 +4,8 @@ using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService;
using Ryujinx.HLE.Utilities;
using System;
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
public ResultCode EnsureSaveData(ServiceCtx context)
{
long uIdLow = context.RequestData.ReadInt64();
long uIdHigh = context.RequestData.ReadInt64();
Logger.PrintStub(LogClass.ServiceAm);
UInt128 userId = new UInt128(context.RequestData.ReadBytes(0x10));
context.ResponseData.Write(0L);
Logger.PrintStub(LogClass.ServiceAm, new { userId });
return ResultCode.Success;
}
@ -54,9 +55,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
// SetTerminateResult(u32)
public ResultCode SetTerminateResult(ServiceCtx context)
{
int errorCode = context.RequestData.ReadInt32();
string result = GetFormattedErrorCode(errorCode);
int errorCode = context.RequestData.ReadInt32();
string result = GetFormattedErrorCode(errorCode);
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
public ResultCode GetPseudoDeviceId(ServiceCtx context)
{
Logger.PrintStub(LogClass.ServiceAm);
context.ResponseData.Write(0L);
context.ResponseData.Write(0L);
context.ResponseData.Write(0L);
context.ResponseData.Write(0L);
Logger.PrintStub(LogClass.ServiceAm);
return ResultCode.Success;
}
@ -118,11 +118,27 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
{
int state = context.RequestData.ReadInt32();
Logger.PrintStub(LogClass.ServiceAm);
Logger.PrintStub(LogClass.ServiceAm, new { state });
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+
// GetGpuErrorDetectedSystemEvent() -> handle<copy>
public ResultCode GetGpuErrorDetectedSystemEvent(ServiceCtx context)

View file

@ -7,7 +7,10 @@ namespace Ryujinx.HLE.HOS.Services.Am
Success = 0,
NotAvailable = (2 << ErrorCodeShift) | ModuleId,
NoMessages = (3 << ErrorCodeShift) | ModuleId,
ObjectInvalid = (500 << ErrorCodeShift) | ModuleId,
OutOfBounds = (503 << ErrorCodeShift) | ModuleId,
CpuBoostModeInvalid = (506 << ErrorCodeShift) | ModuleId
}
}

View file

@ -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")]
class IQueryService : IpcService
{
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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types
{
enum PlayLogQueryCapability
{
None,
WhiteList,
All
}
}

View 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
}
}