Move solution and projects to src

This commit is contained in:
TSR Berry 2023-04-08 01:22:00 +02:00 committed by Mary
parent cd124bda58
commit cee7121058
3466 changed files with 55 additions and 55 deletions

View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ryujinx.Tests")]

View file

@ -0,0 +1,11 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
public class GuestBrokeExecutionException : Exception
{
private const string ExMsg = "The guest program broke execution!";
public GuestBrokeExecutionException() : base(ExMsg) { }
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
class InternalServiceException: Exception
{
public InternalServiceException(string message) : base(message) { }
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
class InvalidFirmwarePackageException : Exception
{
public InvalidFirmwarePackageException(string message) : base(message) { }
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
public class InvalidNpdmException : Exception
{
public InvalidNpdmException(string message) : base(message) { }
}
}

View file

@ -0,0 +1,15 @@
using System;
using System.Runtime.CompilerServices;
namespace Ryujinx.HLE.Exceptions
{
public class InvalidStructLayoutException<T> : Exception
{
static readonly Type _structType = typeof(T);
public InvalidStructLayoutException(string message) : base(message) { }
public InvalidStructLayoutException(int expectedSize)
: base($"Type {_structType.Name} has the wrong size. Expected: {expectedSize} bytes, got: {Unsafe.SizeOf<T>()} bytes") { }
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
public class InvalidSystemResourceException : Exception
{
public InvalidSystemResourceException(string message) : base(message) { }
}
}

View file

@ -0,0 +1,164 @@
using Ryujinx.Common;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Services;
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
namespace Ryujinx.HLE.Exceptions
{
[Serializable]
internal class ServiceNotImplementedException : Exception
{
public IpcService Service { get; }
public ServiceCtx Context { get; }
public IpcMessage Request { get; }
public ServiceNotImplementedException(IpcService service, ServiceCtx context)
: this(service, context, "The service call is not implemented.") { }
public ServiceNotImplementedException(IpcService service, ServiceCtx context, string message) : base(message)
{
Service = service;
Context = context;
Request = context.Request;
}
public ServiceNotImplementedException(IpcService service, ServiceCtx context, string message, Exception inner) : base(message, inner)
{
Service = service;
Context = context;
Request = context.Request;
}
protected ServiceNotImplementedException(SerializationInfo info, StreamingContext context) : base(info, context) { }
public override string Message
{
get
{
return base.Message + Environment.NewLine + Environment.NewLine + BuildMessage();
}
}
private string BuildMessage()
{
StringBuilder sb = new StringBuilder();
// Print the IPC command details (service name, command ID, and handler)
(Type callingType, MethodBase callingMethod) = WalkStackTrace(new StackTrace(this));
if (callingType != null && callingMethod != null)
{
// If the type is past 0xF, we are using TIPC
var ipcCommands = Request.Type > IpcMessageType.TipcCloseSession ? Service.TipcCommands : Service.CmifCommands;
// Find the handler for the method called
var ipcHandler = ipcCommands.FirstOrDefault(x => x.Value == callingMethod);
var ipcCommandId = ipcHandler.Key;
var ipcMethod = ipcHandler.Value;
if (ipcMethod != null)
{
sb.AppendLine($"Service Command: {Service.GetType().FullName}: {ipcCommandId} ({ipcMethod.Name})");
sb.AppendLine();
}
}
sb.AppendLine("Guest Stack Trace:");
sb.AppendLine(Context.Thread.GetGuestStackTrace());
// Print buffer information
if (Request.PtrBuff.Count > 0 ||
Request.SendBuff.Count > 0 ||
Request.ReceiveBuff.Count > 0 ||
Request.ExchangeBuff.Count > 0 ||
Request.RecvListBuff.Count > 0)
{
sb.AppendLine("Buffer Information:");
if (Request.PtrBuff.Count > 0)
{
sb.AppendLine("\tPtrBuff:");
foreach (var buff in Request.PtrBuff)
{
sb.AppendLine($"\t[{buff.Index}] Position: 0x{buff.Position:x16} Size: 0x{buff.Size:x16}");
}
}
if (Request.SendBuff.Count > 0)
{
sb.AppendLine("\tSendBuff:");
foreach (var buff in Request.SendBuff)
{
sb.AppendLine($"\tPosition: 0x{buff.Position:x16} Size: 0x{buff.Size:x16} Flags: {buff.Flags}");
}
}
if (Request.ReceiveBuff.Count > 0)
{
sb.AppendLine("\tReceiveBuff:");
foreach (var buff in Request.ReceiveBuff)
{
sb.AppendLine($"\tPosition: 0x{buff.Position:x16} Size: 0x{buff.Size:x16} Flags: {buff.Flags}");
}
}
if (Request.ExchangeBuff.Count > 0)
{
sb.AppendLine("\tExchangeBuff:");
foreach (var buff in Request.ExchangeBuff)
{
sb.AppendLine($"\tPosition: 0x{buff.Position:x16} Size: 0x{buff.Size:x16} Flags: {buff.Flags}");
}
}
if (Request.RecvListBuff.Count > 0)
{
sb.AppendLine("\tRecvListBuff:");
foreach (var buff in Request.RecvListBuff)
{
sb.AppendLine($"\tPosition: 0x{buff.Position:x16} Size: 0x{buff.Size:x16}");
}
}
sb.AppendLine();
}
sb.AppendLine("Raw Request Data:");
sb.Append(HexUtils.HexTable(Request.RawData));
return sb.ToString();
}
private static (Type, MethodBase) WalkStackTrace(StackTrace trace)
{
int i = 0;
StackFrame frame;
// Find the IIpcService method that threw this exception
while ((frame = trace.GetFrame(i++)) != null)
{
var method = frame.GetMethod();
var declType = method.DeclaringType;
if (typeof(IpcService).IsAssignableFrom(declType))
{
return (declType, method);
}
}
return (null, null);
}
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
public class TamperCompilationException : Exception
{
public TamperCompilationException(string message) : base(message) { }
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
public class TamperExecutionException : Exception
{
public TamperExecutionException(string message) : base(message) { }
}
}

View file

@ -0,0 +1,13 @@
using System;
namespace Ryujinx.HLE.Exceptions
{
public class UndefinedInstructionException : Exception
{
private const string ExMsg = "The instruction at 0x{0:x16} (opcode 0x{1:x8}) is undefined!";
public UndefinedInstructionException() : base() { }
public UndefinedInstructionException(ulong address, int opCode) : base(string.Format(ExMsg, address, opCode)) { }
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,82 @@
using LibHac.Fs;
using LibHac.Ncm;
using Ryujinx.Common.Configuration;
using System;
using static Ryujinx.HLE.FileSystem.VirtualFileSystem;
using Path = System.IO.Path;
namespace Ryujinx.HLE.FileSystem
{
internal static class ContentPath
{
public const string SystemContent = "@SystemContent";
public const string UserContent = "@UserContent";
public const string SdCardContent = "@SdCardContent";
public const string SdCard = "@Sdcard";
public const string CalibFile = "@CalibFile";
public const string Safe = "@Safe";
public const string User = "@User";
public const string System = "@System";
public const string Host = "@Host";
public const string GamecardApp = "@GcApp";
public const string GamecardContents = "@GcS00000001";
public const string GamecardUpdate = "@upp";
public const string RegisteredUpdate = "@RegUpdate";
public const string Nintendo = "Nintendo";
public const string Contents = "Contents";
public static string GetRealPath(VirtualFileSystem fileSystem, string switchContentPath)
{
return switchContentPath switch
{
SystemContent => Path.Combine(AppDataManager.BaseDirPath, SystemNandPath, Contents),
UserContent => Path.Combine(AppDataManager.BaseDirPath, UserNandPath, Contents),
SdCardContent => Path.Combine(fileSystem.GetSdCardPath(), Nintendo, Contents),
System => Path.Combine(AppDataManager.BaseDirPath, SystemNandPath),
User => Path.Combine(AppDataManager.BaseDirPath, UserNandPath),
_ => throw new NotSupportedException($"Content Path \"`{switchContentPath}`\" is not supported.")
};
}
public static string GetContentPath(ContentStorageId contentStorageId)
{
return contentStorageId switch
{
ContentStorageId.System => SystemContent,
ContentStorageId.User => UserContent,
ContentStorageId.SdCard => SdCardContent,
_ => throw new NotSupportedException($"Content Storage Id \"`{contentStorageId}`\" is not supported.")
};
}
public static string GetContentPath(StorageId storageId)
{
return storageId switch
{
StorageId.BuiltInSystem => SystemContent,
StorageId.BuiltInUser => UserContent,
StorageId.SdCard => SdCardContent,
_ => throw new NotSupportedException($"Storage Id \"`{storageId}`\" is not supported.")
};
}
public static StorageId GetStorageId(string contentPathString)
{
return contentPathString.Split(':')[0] switch
{
SystemContent or
System => StorageId.BuiltInSystem,
UserContent or
User => StorageId.BuiltInUser,
SdCardContent => StorageId.SdCard,
Host => StorageId.Host,
GamecardApp or
GamecardContents or
GamecardUpdate => StorageId.GameCard,
_ => StorageId.None
};
}
}
}

View file

@ -0,0 +1,26 @@
using LibHac;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSrv.FsCreator;
namespace Ryujinx.HLE.FileSystem
{
public class EncryptedFileSystemCreator : IEncryptedFileSystemCreator
{
public Result Create(ref SharedRef<IFileSystem> outEncryptedFileSystem,
ref SharedRef<IFileSystem> baseFileSystem, IEncryptedFileSystemCreator.KeyId idIndex,
in EncryptionSeed encryptionSeed)
{
if (idIndex < IEncryptedFileSystemCreator.KeyId.Save || idIndex > IEncryptedFileSystemCreator.KeyId.CustomStorage)
{
return ResultFs.InvalidArgument.Log();
}
// TODO: Reenable when AesXtsFileSystem is fixed.
outEncryptedFileSystem = SharedRef<IFileSystem>.CreateMove(ref baseFileSystem);
return Result.Success;
}
}
}

View file

@ -0,0 +1,25 @@
using LibHac.Tools.FsSystem.NcaUtils;
namespace Ryujinx.HLE.FileSystem
{
public struct LocationEntry
{
public string ContentPath { get; private set; }
public int Flag { get; private set; }
public ulong TitleId { get; private set; }
public NcaContentType ContentType { get; private set; }
public LocationEntry(string contentPath, int flag, ulong titleId, NcaContentType contentType)
{
ContentPath = contentPath;
Flag = flag;
TitleId = titleId;
ContentType = contentType;
}
public void SetFlag(int flag)
{
Flag = flag;
}
}
}

View file

@ -0,0 +1,40 @@
using Ryujinx.HLE.Utilities;
using System.IO;
namespace Ryujinx.HLE.FileSystem
{
public class SystemVersion
{
public byte Major { get; }
public byte Minor { get; }
public byte Micro { get; }
public byte RevisionMajor { get; }
public byte RevisionMinor { get; }
public string PlatformString { get; }
public string Hex { get; }
public string VersionString { get; }
public string VersionTitle { get; }
public SystemVersion(Stream systemVersionFile)
{
using (BinaryReader reader = new BinaryReader(systemVersionFile))
{
Major = reader.ReadByte();
Minor = reader.ReadByte();
Micro = reader.ReadByte();
reader.ReadByte(); // Padding
RevisionMajor = reader.ReadByte();
RevisionMinor = reader.ReadByte();
reader.ReadBytes(2); // Padding
PlatformString = StringUtils.ReadInlinedAsciiString(reader, 0x20);
Hex = StringUtils.ReadInlinedAsciiString(reader, 0x40);
VersionString = StringUtils.ReadInlinedAsciiString(reader, 0x18);
VersionTitle = StringUtils.ReadInlinedAsciiString(reader, 0x80);
}
}
}
}

View file

@ -0,0 +1,615 @@
using LibHac;
using LibHac.Common;
using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.Fs.Shim;
using LibHac.FsSrv;
using LibHac.FsSystem;
using LibHac.Ncm;
using LibHac.Spl;
using LibHac.Tools.Es;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS;
using System;
using System.Buffers.Text;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using Path = System.IO.Path;
using RightsId = LibHac.Fs.RightsId;
namespace Ryujinx.HLE.FileSystem
{
public class VirtualFileSystem : IDisposable
{
public static string SafeNandPath = Path.Combine(AppDataManager.DefaultNandDir, "safe");
public static string SystemNandPath = Path.Combine(AppDataManager.DefaultNandDir, "system");
public static string UserNandPath = Path.Combine(AppDataManager.DefaultNandDir, "user");
public KeySet KeySet { get; private set; }
public EmulatedGameCard GameCard { get; private set; }
public EmulatedSdCard SdCard { get; private set; }
public ModLoader ModLoader { get; private set; }
private readonly ConcurrentDictionary<ulong, Stream> _romFsByPid;
private static bool _isInitialized = false;
public static VirtualFileSystem CreateInstance()
{
if (_isInitialized)
{
throw new InvalidOperationException("VirtualFileSystem can only be instantiated once!");
}
_isInitialized = true;
return new VirtualFileSystem();
}
private VirtualFileSystem()
{
ReloadKeySet();
ModLoader = new ModLoader(); // Should only be created once
_romFsByPid = new ConcurrentDictionary<ulong, Stream>();
}
public void LoadRomFs(ulong pid, string fileName)
{
var romfsStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
_romFsByPid.AddOrUpdate(pid, romfsStream, (pid, oldStream) =>
{
oldStream.Close();
return romfsStream;
});
}
public void SetRomFs(ulong pid, Stream romfsStream)
{
_romFsByPid.AddOrUpdate(pid, romfsStream, (pid, oldStream) =>
{
oldStream.Close();
return romfsStream;
});
}
public Stream GetRomFs(ulong pid)
{
return _romFsByPid[pid];
}
public string GetFullPath(string basePath, string fileName)
{
if (fileName.StartsWith("//"))
{
fileName = fileName.Substring(2);
}
else if (fileName.StartsWith('/'))
{
fileName = fileName.Substring(1);
}
else
{
return null;
}
string fullPath = Path.GetFullPath(Path.Combine(basePath, fileName));
if (!fullPath.StartsWith(AppDataManager.BaseDirPath))
{
return null;
}
return fullPath;
}
internal string GetSdCardPath() => MakeFullPath(AppDataManager.DefaultSdcardDir);
public string GetNandPath() => MakeFullPath(AppDataManager.DefaultNandDir);
public string SwitchPathToSystemPath(string switchPath)
{
string[] parts = switchPath.Split(":");
if (parts.Length != 2)
{
return null;
}
return GetFullPath(MakeFullPath(parts[0]), parts[1]);
}
public string SystemPathToSwitchPath(string systemPath)
{
string baseSystemPath = AppDataManager.BaseDirPath + Path.DirectorySeparatorChar;
if (systemPath.StartsWith(baseSystemPath))
{
string rawPath = systemPath.Replace(baseSystemPath, "");
int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar);
if (firstSeparatorOffset == -1)
{
return $"{rawPath}:/";
}
var basePath = rawPath.AsSpan(0, firstSeparatorOffset);
var fileName = rawPath.AsSpan(firstSeparatorOffset + 1);
return $"{basePath}:/{fileName}";
}
return null;
}
private string MakeFullPath(string path, bool isDirectory = true)
{
// Handles Common Switch Content Paths
switch (path)
{
case ContentPath.SdCard:
path = AppDataManager.DefaultSdcardDir;
break;
case ContentPath.User:
path = UserNandPath;
break;
case ContentPath.System:
path = SystemNandPath;
break;
case ContentPath.SdCardContent:
path = Path.Combine(AppDataManager.DefaultSdcardDir, "Nintendo", "Contents");
break;
case ContentPath.UserContent:
path = Path.Combine(UserNandPath, "Contents");
break;
case ContentPath.SystemContent:
path = Path.Combine(SystemNandPath, "Contents");
break;
}
string fullPath = Path.Combine(AppDataManager.BaseDirPath, path);
if (isDirectory && !Directory.Exists(fullPath))
{
Directory.CreateDirectory(fullPath);
}
return fullPath;
}
public void InitializeFsServer(LibHac.Horizon horizon, out HorizonClient fsServerClient)
{
LocalFileSystem serverBaseFs = new LocalFileSystem(AppDataManager.BaseDirPath);
fsServerClient = horizon.CreatePrivilegedHorizonClient();
var fsServer = new FileSystemServer(fsServerClient);
RandomDataGenerator randomGenerator = Random.Shared.NextBytes;
DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet, fsServer, randomGenerator);
// Use our own encrypted fs creator that doesn't actually do any encryption
fsServerObjects.FsCreators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator();
GameCard = fsServerObjects.GameCard;
SdCard = fsServerObjects.SdCard;
SdCard.SetSdCardInsertionStatus(true);
var fsServerConfig = new FileSystemServerConfig
{
DeviceOperator = fsServerObjects.DeviceOperator,
ExternalKeySet = KeySet.ExternalKeySet,
FsCreators = fsServerObjects.FsCreators,
RandomGenerator = randomGenerator
};
FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig);
}
public void ReloadKeySet()
{
KeySet ??= KeySet.CreateDefaultKeySet();
string keyFile = null;
string titleKeyFile = null;
string consoleKeyFile = null;
if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile)
{
LoadSetAtPath(AppDataManager.KeysDirPathUser);
}
LoadSetAtPath(AppDataManager.KeysDirPath);
void LoadSetAtPath(string basePath)
{
string localKeyFile = Path.Combine(basePath, "prod.keys");
string localTitleKeyFile = Path.Combine(basePath, "title.keys");
string localConsoleKeyFile = Path.Combine(basePath, "console.keys");
if (File.Exists(localKeyFile))
{
keyFile = localKeyFile;
}
if (File.Exists(localTitleKeyFile))
{
titleKeyFile = localTitleKeyFile;
}
if (File.Exists(localConsoleKeyFile))
{
consoleKeyFile = localConsoleKeyFile;
}
}
ExternalKeyReader.ReadKeyFile(KeySet, keyFile, titleKeyFile, consoleKeyFile, null);
}
public void ImportTickets(IFileSystem fs)
{
foreach (DirectoryEntryEx ticketEntry in fs.EnumerateEntries("/", "*.tik"))
{
using var ticketFile = new UniqueRef<IFile>();
Result result = fs.OpenFile(ref ticketFile.Ref, ticketEntry.FullPath.ToU8Span(), OpenMode.Read);
if (result.IsSuccess())
{
Ticket ticket = new(ticketFile.Get.AsStream());
var titleKey = ticket.GetTitleKey(KeySet);
if (titleKey != null)
{
KeySet.ExternalKeySet.Add(new RightsId(ticket.RightsId), new AccessKey(titleKey));
}
}
}
}
// Save data created before we supported extra data in directory save data will not work properly if
// given empty extra data. Luckily some of that extra data can be created using the data from the
// save data indexer, which should be enough to check access permissions for user saves.
// Every single save data's extra data will be checked and fixed if needed each time the emulator is opened.
// Consider removing this at some point in the future when we don't need to worry about old saves.
public static Result FixExtraData(HorizonClient hos)
{
Result rc = GetSystemSaveList(hos, out List<ulong> systemSaveIds);
if (rc.IsFailure()) return rc;
rc = FixUnindexedSystemSaves(hos, systemSaveIds);
if (rc.IsFailure()) return rc;
rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.System);
if (rc.IsFailure()) return rc;
rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.User);
if (rc.IsFailure()) return rc;
return Result.Success;
}
private static Result FixExtraDataInSpaceId(HorizonClient hos, SaveDataSpaceId spaceId)
{
Span<SaveDataInfo> info = stackalloc SaveDataInfo[8];
using var iterator = new UniqueRef<SaveDataIterator>();
Result rc = hos.Fs.OpenSaveDataIterator(ref iterator.Ref, spaceId);
if (rc.IsFailure()) return rc;
while (true)
{
rc = iterator.Get.ReadSaveDataInfo(out long count, info);
if (rc.IsFailure()) return rc;
if (count == 0)
return Result.Success;
for (int i = 0; i < count; i++)
{
rc = FixExtraData(out bool wasFixNeeded, hos, in info[i]);
if (ResultFs.TargetNotFound.Includes(rc))
{
// If the save wasn't found, try to create the directory for its save data ID
rc = CreateSaveDataDirectory(hos, in info[i]);
if (rc.IsFailure())
{
Logger.Warning?.Print(LogClass.Application, $"Error {rc.ToStringWithName()} when creating save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space");
// Don't bother fixing the extra data if we couldn't create the directory
continue;
}
Logger.Info?.Print(LogClass.Application, $"Recreated directory for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space");
// Try to fix the extra data in the new directory
rc = FixExtraData(out wasFixNeeded, hos, in info[i]);
}
if (rc.IsFailure())
{
Logger.Warning?.Print(LogClass.Application, $"Error {rc.ToStringWithName()} when fixing extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space");
}
else if (wasFixNeeded)
{
Logger.Info?.Print(LogClass.Application, $"Fixed extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space");
}
}
}
}
private static Result CreateSaveDataDirectory(HorizonClient hos, in SaveDataInfo info)
{
if (info.SpaceId != SaveDataSpaceId.User && info.SpaceId != SaveDataSpaceId.System)
return Result.Success;
const string mountName = "SaveDir";
var mountNameU8 = mountName.ToU8Span();
BisPartitionId partitionId = info.SpaceId switch
{
SaveDataSpaceId.System => BisPartitionId.System,
SaveDataSpaceId.User => BisPartitionId.User,
_ => throw new ArgumentOutOfRangeException()
};
Result rc = hos.Fs.MountBis(mountNameU8, partitionId);
if (rc.IsFailure()) return rc;
try
{
var path = $"{mountName}:/save/{info.SaveDataId:x16}".ToU8Span();
rc = hos.Fs.GetEntryType(out _, path);
if (ResultFs.PathNotFound.Includes(rc))
{
rc = hos.Fs.CreateDirectory(path);
}
return rc;
}
finally
{
hos.Fs.Unmount(mountNameU8);
}
}
// Gets a list of all the save data files or directories in the system partition.
private static Result GetSystemSaveList(HorizonClient hos, out List<ulong> list)
{
list = null;
var mountName = "system".ToU8Span();
DirectoryHandle handle = default;
List<ulong> localList = new List<ulong>();
try
{
Result rc = hos.Fs.MountBis(mountName, BisPartitionId.System);
if (rc.IsFailure()) return rc;
rc = hos.Fs.OpenDirectory(out handle, "system:/save".ToU8Span(), OpenDirectoryMode.All);
if (rc.IsFailure()) return rc;
DirectoryEntry entry = new DirectoryEntry();
while (true)
{
rc = hos.Fs.ReadDirectory(out long readCount, SpanHelpers.AsSpan(ref entry), handle);
if (rc.IsFailure()) return rc;
if (readCount == 0)
break;
if (Utf8Parser.TryParse(entry.Name, out ulong saveDataId, out int bytesRead, 'x') &&
bytesRead == 16 && (long)saveDataId < 0)
{
localList.Add(saveDataId);
}
}
list = localList;
return Result.Success;
}
finally
{
if (handle.IsValid)
{
hos.Fs.CloseDirectory(handle);
}
if (hos.Fs.IsMounted(mountName))
{
hos.Fs.Unmount(mountName);
}
}
}
// Adds system save data that isn't in the save data indexer to the indexer and creates extra data for it.
// Only save data IDs added to SystemExtraDataFixInfo will be fixed.
private static Result FixUnindexedSystemSaves(HorizonClient hos, List<ulong> existingSaveIds)
{
foreach (var fixInfo in SystemExtraDataFixInfo)
{
if (!existingSaveIds.Contains(fixInfo.StaticSaveDataId))
{
continue;
}
Result rc = FixSystemExtraData(out bool wasFixNeeded, hos, in fixInfo);
if (rc.IsFailure())
{
Logger.Warning?.Print(LogClass.Application,
$"Error {rc.ToStringWithName()} when fixing extra data for system save data 0x{fixInfo.StaticSaveDataId:x}");
}
else if (wasFixNeeded)
{
Logger.Info?.Print(LogClass.Application,
$"Tried to rebuild extra data for system save data 0x{fixInfo.StaticSaveDataId:x}");
}
}
return Result.Success;
}
private static Result FixSystemExtraData(out bool wasFixNeeded, HorizonClient hos, in ExtraDataFixInfo info)
{
wasFixNeeded = true;
Result rc = hos.Fs.Impl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData extraData, info.StaticSaveDataId);
if (!rc.IsSuccess())
{
if (!ResultFs.TargetNotFound.Includes(rc))
return rc;
// We'll reach this point only if the save data directory exists but it's not in the save data indexer.
// Creating the save will add it to the indexer while leaving its existing contents intact.
return hos.Fs.CreateSystemSaveData(info.StaticSaveDataId, UserId.InvalidId, info.OwnerId, info.DataSize,
info.JournalSize, info.Flags);
}
if (extraData.Attribute.StaticSaveDataId != 0 && extraData.OwnerId != 0)
{
wasFixNeeded = false;
return Result.Success;
}
extraData = new SaveDataExtraData
{
Attribute = { StaticSaveDataId = info.StaticSaveDataId },
OwnerId = info.OwnerId,
Flags = info.Flags,
DataSize = info.DataSize,
JournalSize = info.JournalSize
};
// Make a mask for writing the entire extra data
Unsafe.SkipInit(out SaveDataExtraData extraDataMask);
SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF);
return hos.Fs.Impl.WriteSaveDataFileSystemExtraData(SaveDataSpaceId.System, info.StaticSaveDataId,
in extraData, in extraDataMask);
}
private static Result FixExtraData(out bool wasFixNeeded, HorizonClient hos, in SaveDataInfo info)
{
wasFixNeeded = true;
Result rc = hos.Fs.Impl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData extraData, info.SpaceId,
info.SaveDataId);
if (rc.IsFailure()) return rc;
// The extra data should have program ID or static save data ID set if it's valid.
// We only try to fix the extra data if the info from the save data indexer has a program ID or static save data ID.
bool canFixByProgramId = extraData.Attribute.ProgramId == ProgramId.InvalidId &&
info.ProgramId != ProgramId.InvalidId;
bool canFixBySaveDataId = extraData.Attribute.StaticSaveDataId == 0 && info.StaticSaveDataId != 0;
bool hasEmptyOwnerId = extraData.OwnerId == 0 && info.Type != SaveDataType.System;
if (!canFixByProgramId && !canFixBySaveDataId && !hasEmptyOwnerId)
{
wasFixNeeded = false;
return Result.Success;
}
// The save data attribute struct can be completely created from the save data info.
extraData.Attribute.ProgramId = info.ProgramId;
extraData.Attribute.UserId = info.UserId;
extraData.Attribute.StaticSaveDataId = info.StaticSaveDataId;
extraData.Attribute.Type = info.Type;
extraData.Attribute.Rank = info.Rank;
extraData.Attribute.Index = info.Index;
// The rest of the extra data can't be created from the save data info.
// On user saves the owner ID will almost certainly be the same as the program ID.
if (info.Type != SaveDataType.System)
{
extraData.OwnerId = info.ProgramId.Value;
}
else
{
// Try to match the system save with one of the known saves
foreach (ExtraDataFixInfo fixInfo in SystemExtraDataFixInfo)
{
if (extraData.Attribute.StaticSaveDataId == fixInfo.StaticSaveDataId)
{
extraData.OwnerId = fixInfo.OwnerId;
extraData.Flags = fixInfo.Flags;
extraData.DataSize = fixInfo.DataSize;
extraData.JournalSize = fixInfo.JournalSize;
break;
}
}
}
// Make a mask for writing the entire extra data
Unsafe.SkipInit(out SaveDataExtraData extraDataMask);
SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF);
return hos.Fs.Impl.WriteSaveDataFileSystemExtraData(info.SpaceId, info.SaveDataId, in extraData, in extraDataMask);
}
struct ExtraDataFixInfo
{
public ulong StaticSaveDataId;
public ulong OwnerId;
public SaveDataFlags Flags;
public long DataSize;
public long JournalSize;
}
private static readonly ExtraDataFixInfo[] SystemExtraDataFixInfo =
{
new ExtraDataFixInfo()
{
StaticSaveDataId = 0x8000000000000030,
OwnerId = 0x010000000000001F,
Flags = SaveDataFlags.KeepAfterResettingSystemSaveDataWithoutUserSaveData,
DataSize = 0x10000,
JournalSize = 0x10000
},
new ExtraDataFixInfo()
{
StaticSaveDataId = 0x8000000000001040,
OwnerId = 0x0100000000001009,
Flags = SaveDataFlags.None,
DataSize = 0xC000,
JournalSize = 0xC000
}
};
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
foreach (var stream in _romFsByPid.Values)
{
stream.Close();
}
_romFsByPid.Clear();
}
}
}
}

View file

@ -0,0 +1,219 @@
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Integration;
using Ryujinx.Common.Configuration;
using Ryujinx.Graphics.GAL;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Ui;
using System;
namespace Ryujinx.HLE
{
/// <summary>
/// HLE configuration.
/// </summary>
public class HLEConfiguration
{
/// <summary>
/// The virtual file system used by the FS service.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly VirtualFileSystem VirtualFileSystem;
/// <summary>
/// The manager for handling a LibHac Horizon instance.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly LibHacHorizonManager LibHacHorizonManager;
/// <summary>
/// The account manager used by the account service.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly AccountManager AccountManager;
/// <summary>
/// The content manager used by the NCM service.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly ContentManager ContentManager;
/// <summary>
/// The persistent information between run for multi-application capabilities.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
public readonly UserChannelPersistence UserChannelPersistence;
/// <summary>
/// The GPU renderer to use for all GPU operations.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly IRenderer GpuRenderer;
/// <summary>
/// The audio device driver to use for all audio operations.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly IHardwareDeviceDriver AudioDeviceDriver;
/// <summary>
/// The handler for various UI related operations needed outside of HLE.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly IHostUiHandler HostUiHandler;
/// <summary>
/// Control the memory configuration used by the emulation context.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly MemoryConfiguration MemoryConfiguration;
/// <summary>
/// The system language to use in the settings service.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly SystemLanguage SystemLanguage;
/// <summary>
/// The system region to use in the settings service.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly RegionCode Region;
/// <summary>
/// Control the initial state of the vertical sync in the SurfaceFlinger service.
/// </summary>
internal readonly bool EnableVsync;
/// <summary>
/// Control the initial state of the docked mode.
/// </summary>
internal readonly bool EnableDockedMode;
/// <summary>
/// Control if the Profiled Translation Cache (PTC) should be used.
/// </summary>
internal readonly bool EnablePtc;
/// <summary>
/// Control if the guest application should be told that there is a Internet connection available.
/// </summary>
internal readonly bool EnableInternetAccess;
/// <summary>
/// Control LibHac's integrity check level.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly IntegrityCheckLevel FsIntegrityCheckLevel;
/// <summary>
/// Control LibHac's global access logging level. Value must be between 0 and 3.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly int FsGlobalAccessLogMode;
/// <summary>
/// The system time offset to apply to the time service steady and local clocks.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly long SystemTimeOffset;
/// <summary>
/// The system timezone used by the time service.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly string TimeZone;
/// <summary>
/// Type of the memory manager used on CPU emulation.
/// </summary>
public MemoryManagerMode MemoryManagerMode { internal get; set; }
/// <summary>
/// Control the initial state of the ignore missing services setting.
/// If this is set to true, when a missing service is encountered, it will try to automatically handle it instead of throwing an exception.
/// </summary>
/// TODO: Update this again.
public bool IgnoreMissingServices { internal get; set; }
/// <summary>
/// Aspect Ratio applied to the renderer window by the SurfaceFlinger service.
/// </summary>
public AspectRatio AspectRatio { get; set; }
/// <summary>
/// The audio volume level.
/// </summary>
public float AudioVolume { get; set; }
/// <summary>
/// Use Hypervisor over JIT if available.
/// </summary>
internal readonly bool UseHypervisor;
/// <summary>
/// Multiplayer LAN Interface ID (device GUID)
/// </summary>
public string MultiplayerLanInterfaceId { internal get; set; }
/// <summary>
/// An action called when HLE force a refresh of output after docked mode changed.
/// </summary>
public Action RefreshInputConfig { internal get; set; }
public HLEConfiguration(VirtualFileSystem virtualFileSystem,
LibHacHorizonManager libHacHorizonManager,
ContentManager contentManager,
AccountManager accountManager,
UserChannelPersistence userChannelPersistence,
IRenderer gpuRenderer,
IHardwareDeviceDriver audioDeviceDriver,
MemoryConfiguration memoryConfiguration,
IHostUiHandler hostUiHandler,
SystemLanguage systemLanguage,
RegionCode region,
bool enableVsync,
bool enableDockedMode,
bool enablePtc,
bool enableInternetAccess,
IntegrityCheckLevel fsIntegrityCheckLevel,
int fsGlobalAccessLogMode,
long systemTimeOffset,
string timeZone,
MemoryManagerMode memoryManagerMode,
bool ignoreMissingServices,
AspectRatio aspectRatio,
float audioVolume,
bool useHypervisor,
string multiplayerLanInterfaceId)
{
VirtualFileSystem = virtualFileSystem;
LibHacHorizonManager = libHacHorizonManager;
AccountManager = accountManager;
ContentManager = contentManager;
UserChannelPersistence = userChannelPersistence;
GpuRenderer = gpuRenderer;
AudioDeviceDriver = audioDeviceDriver;
MemoryConfiguration = memoryConfiguration;
HostUiHandler = hostUiHandler;
SystemLanguage = systemLanguage;
Region = region;
EnableVsync = enableVsync;
EnableDockedMode = enableDockedMode;
EnablePtc = enablePtc;
EnableInternetAccess = enableInternetAccess;
FsIntegrityCheckLevel = fsIntegrityCheckLevel;
FsGlobalAccessLogMode = fsGlobalAccessLogMode;
SystemTimeOffset = systemTimeOffset;
TimeZone = timeZone;
MemoryManagerMode = memoryManagerMode;
IgnoreMissingServices = ignoreMissingServices;
AspectRatio = aspectRatio;
AudioVolume = audioVolume;
UseHypervisor = useHypervisor;
MultiplayerLanInterfaceId = multiplayerLanInterfaceId;
}
}
}

View file

@ -0,0 +1,37 @@
using Ryujinx.HLE.HOS.Applets.Browser;
using Ryujinx.HLE.HOS.Applets.Error;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using System;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Applets
{
static class AppletManager
{
private static Dictionary<AppletId, Type> _appletMapping;
static AppletManager()
{
_appletMapping = new Dictionary<AppletId, Type>
{
{ AppletId.Error, typeof(ErrorApplet) },
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) },
{ AppletId.Controller, typeof(ControllerApplet) },
{ AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) },
{ AppletId.LibAppletWeb, typeof(BrowserApplet) },
{ AppletId.LibAppletShop, typeof(BrowserApplet) },
{ AppletId.LibAppletOff, typeof(BrowserApplet) }
};
}
public static IApplet Create(AppletId applet, Horizon system)
{
if (_appletMapping.TryGetValue(applet, out Type appletClass))
{
return (IApplet)Activator.CreateInstance(appletClass, system);
}
throw new NotImplementedException($"{applet} applet is not implemented.");
}
}
}

View file

@ -0,0 +1,11 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum BootDisplayKind
{
White,
Offline,
Black,
Share,
Lobby
}
}

View file

@ -0,0 +1,104 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using System;
using System.Collections.Generic;
using System.IO;
namespace Ryujinx.HLE.HOS.Applets.Browser
{
internal class BrowserApplet : IApplet
{
public event EventHandler AppletStateChanged;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
private CommonArguments _commonArguments;
private List<BrowserArgument> _arguments;
private ShimKind _shimKind;
public BrowserApplet(Horizon system) {}
public ResultCode GetResult()
{
return ResultCode.Success;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
_normalSession = normalSession;
_interactiveSession = interactiveSession;
_commonArguments = IApplet.ReadStruct<CommonArguments>(_normalSession.Pop());
Logger.Stub?.PrintStub(LogClass.ServiceAm, $"WebApplet version: 0x{_commonArguments.AppletVersion:x8}");
ReadOnlySpan<byte> webArguments = _normalSession.Pop();
(_shimKind, _arguments) = BrowserArgument.ParseArguments(webArguments);
Logger.Stub?.PrintStub(LogClass.ServiceAm, $"Web Arguments: {_arguments.Count}");
foreach (BrowserArgument argument in _arguments)
{
Logger.Stub?.PrintStub(LogClass.ServiceAm, $"{argument.Type}: {argument.GetValue()}");
}
if ((_commonArguments.AppletVersion >= 0x80000 && _shimKind == ShimKind.Web) || (_commonArguments.AppletVersion >= 0x30000 && _shimKind == ShimKind.Share))
{
List<BrowserOutput> result = new List<BrowserOutput>();
result.Add(new BrowserOutput(BrowserOutputType.ExitReason, (uint)WebExitReason.ExitButton));
_normalSession.Push(BuildResponseNew(result));
}
else
{
WebCommonReturnValue result = new WebCommonReturnValue()
{
ExitReason = WebExitReason.ExitButton,
};
_normalSession.Push(BuildResponseOld(result));
}
AppletStateChanged?.Invoke(this, null);
return ResultCode.Success;
}
private byte[] BuildResponseOld(WebCommonReturnValue result)
{
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.WriteStruct(result);
return stream.ToArray();
}
}
private byte[] BuildResponseNew(List<BrowserOutput> outputArguments)
{
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.WriteStruct(new WebArgHeader
{
Count = (ushort)outputArguments.Count,
ShimKind = _shimKind
});
foreach (BrowserOutput output in outputArguments)
{
output.Write(writer);
}
writer.Write(new byte[0x2000 - writer.BaseStream.Position]);
return stream.ToArray();
}
}
}
}

View file

@ -0,0 +1,133 @@
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
namespace Ryujinx.HLE.HOS.Applets.Browser
{
class BrowserArgument
{
public WebArgTLVType Type { get; }
public byte[] Value { get; }
public BrowserArgument(WebArgTLVType type, byte[] value)
{
Type = type;
Value = value;
}
private static readonly Dictionary<WebArgTLVType, Type> _typeRegistry = new Dictionary<WebArgTLVType, Type>
{
{ WebArgTLVType.InitialURL, typeof(string) },
{ WebArgTLVType.CallbackUrl, typeof(string) },
{ WebArgTLVType.CallbackableUrl, typeof(string) },
{ WebArgTLVType.ApplicationId, typeof(ulong) },
{ WebArgTLVType.DocumentPath, typeof(string) },
{ WebArgTLVType.DocumentKind, typeof(DocumentKind) },
{ WebArgTLVType.SystemDataId, typeof(ulong) },
{ WebArgTLVType.Whitelist, typeof(string) },
{ WebArgTLVType.NewsFlag, typeof(bool) },
{ WebArgTLVType.UserID, typeof(UserId) },
{ WebArgTLVType.ScreenShotEnabled, typeof(bool) },
{ WebArgTLVType.EcClientCertEnabled, typeof(bool) },
{ WebArgTLVType.UnknownFlag0x14, typeof(bool) },
{ WebArgTLVType.UnknownFlag0x15, typeof(bool) },
{ WebArgTLVType.PlayReportEnabled, typeof(bool) },
{ WebArgTLVType.BootDisplayKind, typeof(BootDisplayKind) },
{ WebArgTLVType.FooterEnabled, typeof(bool) },
{ WebArgTLVType.PointerEnabled, typeof(bool) },
{ WebArgTLVType.LeftStickMode, typeof(LeftStickMode) },
{ WebArgTLVType.KeyRepeatFrame1, typeof(int) },
{ WebArgTLVType.KeyRepeatFrame2, typeof(int) },
{ WebArgTLVType.BootAsMediaPlayerInverted, typeof(bool) },
{ WebArgTLVType.DisplayUrlKind, typeof(bool) },
{ WebArgTLVType.BootAsMediaPlayer, typeof(bool) },
{ WebArgTLVType.ShopJumpEnabled, typeof(bool) },
{ WebArgTLVType.MediaAutoPlayEnabled, typeof(bool) },
{ WebArgTLVType.LobbyParameter, typeof(string) },
{ WebArgTLVType.JsExtensionEnabled, typeof(bool) },
{ WebArgTLVType.AdditionalCommentText, typeof(string) },
{ WebArgTLVType.TouchEnabledOnContents, typeof(bool) },
{ WebArgTLVType.UserAgentAdditionalString, typeof(string) },
{ WebArgTLVType.MediaPlayerAutoCloseEnabled, typeof(bool) },
{ WebArgTLVType.PageCacheEnabled, typeof(bool) },
{ WebArgTLVType.WebAudioEnabled, typeof(bool) },
{ WebArgTLVType.PageFadeEnabled, typeof(bool) },
{ WebArgTLVType.BootLoadingIconEnabled, typeof(bool) },
{ WebArgTLVType.PageScrollIndicatorEnabled, typeof(bool) },
{ WebArgTLVType.MediaPlayerSpeedControlEnabled, typeof(bool) },
{ WebArgTLVType.OverrideWebAudioVolume, typeof(float) },
{ WebArgTLVType.OverrideMediaAudioVolume, typeof(float) },
{ WebArgTLVType.MediaPlayerUiEnabled, typeof(bool) },
};
public static (ShimKind, List<BrowserArgument>) ParseArguments(ReadOnlySpan<byte> data)
{
List<BrowserArgument> browserArguments = new List<BrowserArgument>();
WebArgHeader header = IApplet.ReadStruct<WebArgHeader>(data.Slice(0, 8));
ReadOnlySpan<byte> rawTLVs = data.Slice(8);
for (int i = 0; i < header.Count; i++)
{
WebArgTLV tlv = IApplet.ReadStruct<WebArgTLV>(rawTLVs);
ReadOnlySpan<byte> tlvData = rawTLVs.Slice(Unsafe.SizeOf<WebArgTLV>(), tlv.Size);
browserArguments.Add(new BrowserArgument((WebArgTLVType)tlv.Type, tlvData.ToArray()));
rawTLVs = rawTLVs.Slice(Unsafe.SizeOf<WebArgTLV>() + tlv.Size);
}
return (header.ShimKind, browserArguments);
}
public object GetValue()
{
if (_typeRegistry.TryGetValue(Type, out Type valueType))
{
if (valueType == typeof(string))
{
return Encoding.UTF8.GetString(Value);
}
else if (valueType == typeof(bool))
{
return Value[0] == 1;
}
else if (valueType == typeof(uint))
{
return BitConverter.ToUInt32(Value);
}
else if (valueType == typeof(int))
{
return BitConverter.ToInt32(Value);
}
else if (valueType == typeof(ulong))
{
return BitConverter.ToUInt64(Value);
}
else if (valueType == typeof(long))
{
return BitConverter.ToInt64(Value);
}
else if (valueType == typeof(float))
{
return BitConverter.ToSingle(Value);
}
else if (valueType == typeof(UserId))
{
return new UserId(Value);
}
else if (valueType.IsEnum)
{
return Enum.ToObject(valueType, BitConverter.ToInt32(Value));
}
return $"{valueType.Name} parsing not implemented";
}
return $"Unknown value format (raw length: {Value.Length})";
}
}
}

View file

@ -0,0 +1,47 @@
using Ryujinx.Common;
using System;
using System.IO;
namespace Ryujinx.HLE.HOS.Applets.Browser
{
class BrowserOutput
{
public BrowserOutputType Type { get; }
public byte[] Value { get; }
public BrowserOutput(BrowserOutputType type, byte[] value)
{
Type = type;
Value = value;
}
public BrowserOutput(BrowserOutputType type, uint value)
{
Type = type;
Value = BitConverter.GetBytes(value);
}
public BrowserOutput(BrowserOutputType type, ulong value)
{
Type = type;
Value = BitConverter.GetBytes(value);
}
public BrowserOutput(BrowserOutputType type, bool value)
{
Type = type;
Value = BitConverter.GetBytes(value);
}
public void Write(BinaryWriter writer)
{
writer.WriteStruct(new WebArgTLV
{
Type = (ushort)Type,
Size = (ushort)Value.Length
});
writer.Write(Value);
}
}
}

View file

@ -0,0 +1,14 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum BrowserOutputType : ushort
{
ExitReason = 0x1,
LastUrl = 0x2,
LastUrlSize = 0x3,
SharePostResult = 0x4,
PostServiceName = 0x5,
PostServiceNameSize = 0x6,
PostId = 0x7,
MediaPlayerAutoClosedByCompletion = 0x8
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum DocumentKind
{
OfflineHtmlPage = 1,
ApplicationLegalInformation,
SystemDataPage
}
}

View file

@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum LeftStickMode
{
Pointer = 0,
Cursor
}
}

View file

@ -0,0 +1,13 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public enum ShimKind : uint
{
Shop = 1,
Login,
Offline,
Share,
Web,
Wifi,
Lobby
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public struct WebArgHeader
{
public ushort Count;
public ushort Padding;
public ShimKind ShimKind;
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public struct WebArgTLV
{
public ushort Type;
public ushort Size;
public uint Padding;
}
}

View file

@ -0,0 +1,62 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
enum WebArgTLVType : ushort
{
InitialURL = 0x1,
CallbackUrl = 0x3,
CallbackableUrl = 0x4,
ApplicationId = 0x5,
DocumentPath = 0x6,
DocumentKind = 0x7,
SystemDataId = 0x8,
ShareStartPage = 0x9,
Whitelist = 0xA,
NewsFlag = 0xB,
UserID = 0xE,
AlbumEntry0 = 0xF,
ScreenShotEnabled = 0x10,
EcClientCertEnabled = 0x11,
PlayReportEnabled = 0x13,
UnknownFlag0x14 = 0x14,
UnknownFlag0x15 = 0x15,
BootDisplayKind = 0x17,
BackgroundKind = 0x18,
FooterEnabled = 0x19,
PointerEnabled = 0x1A,
LeftStickMode = 0x1B,
KeyRepeatFrame1 = 0x1C,
KeyRepeatFrame2 = 0x1D,
BootAsMediaPlayerInverted = 0x1E,
DisplayUrlKind = 0x1F,
BootAsMediaPlayer = 0x21,
ShopJumpEnabled = 0x22,
MediaAutoPlayEnabled = 0x23,
LobbyParameter = 0x24,
ApplicationAlbumEntry = 0x26,
JsExtensionEnabled = 0x27,
AdditionalCommentText = 0x28,
TouchEnabledOnContents = 0x29,
UserAgentAdditionalString = 0x2A,
AdditionalMediaData0 = 0x2B,
MediaPlayerAutoCloseEnabled = 0x2C,
PageCacheEnabled = 0x2D,
WebAudioEnabled = 0x2E,
FooterFixedKind = 0x32,
PageFadeEnabled = 0x33,
MediaCreatorApplicationRatingAge = 0x34,
BootLoadingIconEnabled = 0x35,
PageScrollIndicatorEnabled = 0x36,
MediaPlayerSpeedControlEnabled = 0x37,
AlbumEntry1 = 0x38,
AlbumEntry2 = 0x39,
AlbumEntry3 = 0x3A,
AdditionalMediaData1 = 0x3B,
AdditionalMediaData2 = 0x3C,
AdditionalMediaData3 = 0x3D,
BootFooterButton = 0x3E,
OverrideWebAudioVolume = 0x3F,
OverrideMediaAudioVolume = 0x40,
BootMode = 0x41,
MediaPlayerUiEnabled = 0x43
}
}

View file

@ -0,0 +1,12 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public struct WebCommonReturnValue
{
public WebExitReason ExitReason;
public uint Padding;
public ByteArray4096 LastUrl;
public ulong LastUrlSize;
}
}

View file

@ -0,0 +1,11 @@
namespace Ryujinx.HLE.HOS.Applets.Browser
{
public enum WebExitReason : uint
{
ExitButton,
BackButton,
Requested,
LastUrl,
ErrorDialog = 7
}
}

View file

@ -0,0 +1,16 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets
{
[StructLayout(LayoutKind.Sequential, Pack = 8)]
struct CommonArguments
{
public uint Version;
public uint StructureSize;
public uint AppletVersion;
public uint ThemeColor;
[MarshalAs(UnmanagedType.I1)]
public bool PlayStartupSound;
public ulong SystemTicks;
}
}

View file

@ -0,0 +1,147 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.Services.Hid.Types;
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static Ryujinx.HLE.HOS.Services.Hid.HidServer.HidUtils;
namespace Ryujinx.HLE.HOS.Applets
{
internal class ControllerApplet : IApplet
{
private Horizon _system;
private AppletSession _normalSession;
public event EventHandler AppletStateChanged;
public ControllerApplet(Horizon system)
{
_system = system;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
_normalSession = normalSession;
byte[] launchParams = _normalSession.Pop();
byte[] controllerSupportArgPrivate = _normalSession.Pop();
ControllerSupportArgPrivate privateArg = IApplet.ReadStruct<ControllerSupportArgPrivate>(controllerSupportArgPrivate);
Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet ArgPriv {privateArg.PrivateSize} {privateArg.ArgSize} {privateArg.Mode} " +
$"HoldType:{(NpadJoyHoldType)privateArg.NpadJoyHoldType} StyleSets:{(ControllerType)privateArg.NpadStyleSet}");
if (privateArg.Mode != ControllerSupportMode.ShowControllerSupport)
{
_normalSession.Push(BuildResponse()); // Dummy response for other modes
AppletStateChanged?.Invoke(this, null);
return ResultCode.Success;
}
byte[] controllerSupportArg = _normalSession.Pop();
ControllerSupportArgHeader argHeader;
if (privateArg.ArgSize == Marshal.SizeOf<ControllerSupportArgV7>())
{
ControllerSupportArgV7 arg = IApplet.ReadStruct<ControllerSupportArgV7>(controllerSupportArg);
argHeader = arg.Header;
Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version 7 EnableExplainText={arg.EnableExplainText != 0}");
// Read enable text here?
}
else if (privateArg.ArgSize == Marshal.SizeOf<ControllerSupportArgVPre7>())
{
ControllerSupportArgVPre7 arg = IApplet.ReadStruct<ControllerSupportArgVPre7>(controllerSupportArg);
argHeader = arg.Header;
Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version Pre-7 EnableExplainText={arg.EnableExplainText != 0}");
// Read enable text here?
}
else
{
Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version Unknown");
argHeader = IApplet.ReadStruct<ControllerSupportArgHeader>(controllerSupportArg); // Read just the header
}
int playerMin = argHeader.PlayerCountMin;
int playerMax = argHeader.PlayerCountMax;
bool singleMode = argHeader.EnableSingleMode != 0;
Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet Arg {playerMin} {playerMax} {argHeader.EnableTakeOverConnection} {argHeader.EnableSingleMode}");
if (singleMode)
{
// Applications can set an arbitrary player range even with SingleMode, so clamp it
playerMin = playerMax = 1;
}
int configuredCount = 0;
PlayerIndex primaryIndex = PlayerIndex.Unknown;
while (!_system.Device.Hid.Npads.Validate(playerMin, playerMax, (ControllerType)privateArg.NpadStyleSet, out configuredCount, out primaryIndex))
{
ControllerAppletUiArgs uiArgs = new ControllerAppletUiArgs
{
PlayerCountMin = playerMin,
PlayerCountMax = playerMax,
SupportedStyles = (ControllerType)privateArg.NpadStyleSet,
SupportedPlayers = _system.Device.Hid.Npads.GetSupportedPlayers(),
IsDocked = _system.State.DockedMode
};
if (!_system.Device.UiHandler.DisplayMessageDialog(uiArgs))
{
break;
}
}
ControllerSupportResultInfo result = new ControllerSupportResultInfo
{
PlayerCount = (sbyte)configuredCount,
SelectedId = (uint)GetNpadIdTypeFromIndex(primaryIndex)
};
Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet ReturnResult {result.PlayerCount} {result.SelectedId}");
_normalSession.Push(BuildResponse(result));
AppletStateChanged?.Invoke(this, null);
_system.ReturnFocus();
return ResultCode.Success;
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
private byte[] BuildResponse(ControllerSupportResultInfo result)
{
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(ref result, Unsafe.SizeOf<ControllerSupportResultInfo>())));
return stream.ToArray();
}
}
private byte[] BuildResponse()
{
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.Write((ulong)ResultCode.Success);
return stream.ToArray();
}
}
}
}

View file

@ -0,0 +1,14 @@
using Ryujinx.HLE.HOS.Services.Hid;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Applets
{
public struct ControllerAppletUiArgs
{
public int PlayerCountMin;
public int PlayerCountMax;
public ControllerType SupportedStyles;
public IEnumerable<PlayerIndex> SupportedPlayers;
public bool IsDocked;
}
}

View file

@ -0,0 +1,18 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets
{
#pragma warning disable CS0649
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ControllerSupportArgHeader
{
public sbyte PlayerCountMin;
public sbyte PlayerCountMax;
public byte EnableTakeOverConnection;
public byte EnableLeftJustify;
public byte EnablePermitJoyDual;
public byte EnableSingleMode;
public byte EnableIdentificationColor;
}
#pragma warning restore CS0649
}

View file

@ -0,0 +1,16 @@
namespace Ryujinx.HLE.HOS.Applets
{
#pragma warning disable CS0649
struct ControllerSupportArgPrivate
{
public uint PrivateSize;
public uint ArgSize;
public byte Flag0;
public byte Flag1;
public ControllerSupportMode Mode;
public byte ControllerSupportCaller;
public uint NpadStyleSet;
public uint NpadJoyHoldType;
}
#pragma warning restore CS0649
}

View file

@ -0,0 +1,26 @@
using Ryujinx.Common.Memory;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets
{
#pragma warning disable CS0649
// (8.0.0+ version)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ControllerSupportArgV7
{
public ControllerSupportArgHeader Header;
public Array8<uint> IdentificationColor;
public byte EnableExplainText;
public ExplainTextStruct ExplainText;
[StructLayout(LayoutKind.Sequential, Size = 8 * 0x81)]
public struct ExplainTextStruct
{
private byte element;
public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref element, 8 * 0x81);
}
}
#pragma warning restore CS0649
}

View file

@ -0,0 +1,26 @@
using Ryujinx.Common.Memory;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets
{
#pragma warning disable CS0649
// (1.0.0+ version)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ControllerSupportArgVPre7
{
public ControllerSupportArgHeader Header;
public Array4<uint> IdentificationColor;
public byte EnableExplainText;
public ExplainTextStruct ExplainText;
[StructLayout(LayoutKind.Sequential, Size = 4 * 0x81)]
public struct ExplainTextStruct
{
private byte element;
public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref element, 4 * 0x81);
}
}
#pragma warning restore CS0649
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Applets
{
enum ControllerSupportMode : byte
{
ShowControllerSupport = 0,
ShowControllerStrapGuide = 1,
ShowControllerFirmwareUpdate = 2
}
}

View file

@ -0,0 +1,16 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets
{
#pragma warning disable CS0649
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ControllerSupportResultInfo
{
public sbyte PlayerCount;
private Array3<byte> _padding;
public uint SelectedId;
public uint Result;
}
#pragma warning restore CS0649
}

View file

@ -0,0 +1,14 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.Error
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ApplicationErrorArg
{
public uint ErrorNumber;
public ulong LanguageCode;
public ByteArray2048 MessageText;
public ByteArray2048 DetailsText;
}
}

View file

@ -0,0 +1,216 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ncm;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using Ryujinx.HLE.HOS.SystemState;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace Ryujinx.HLE.HOS.Applets.Error
{
internal partial class ErrorApplet : IApplet
{
private const long ErrorMessageBinaryTitleId = 0x0100000000000801;
private Horizon _horizon;
private AppletSession _normalSession;
private CommonArguments _commonArguments;
private ErrorCommonHeader _errorCommonHeader;
private byte[] _errorStorage;
public event EventHandler AppletStateChanged;
[GeneratedRegex(@"[^\u0000\u0009\u000A\u000D\u0020-\uFFFF]..")]
private static partial Regex CleanTextRegex();
public ErrorApplet(Horizon horizon)
{
_horizon = horizon;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
_normalSession = normalSession;
_commonArguments = IApplet.ReadStruct<CommonArguments>(_normalSession.Pop());
Logger.Info?.PrintMsg(LogClass.ServiceAm, $"ErrorApplet version: 0x{_commonArguments.AppletVersion:x8}");
_errorStorage = _normalSession.Pop();
_errorCommonHeader = IApplet.ReadStruct<ErrorCommonHeader>(_errorStorage);
_errorStorage = _errorStorage.Skip(Marshal.SizeOf<ErrorCommonHeader>()).ToArray();
switch (_errorCommonHeader.Type)
{
case ErrorType.ErrorCommonArg:
{
ParseErrorCommonArg();
break;
}
case ErrorType.ApplicationErrorArg:
{
ParseApplicationErrorArg();
break;
}
default: throw new NotImplementedException($"ErrorApplet type {_errorCommonHeader.Type} is not implemented.");
}
AppletStateChanged?.Invoke(this, null);
return ResultCode.Success;
}
private (uint module, uint description) HexToResultCode(uint resultCode)
{
return ((resultCode & 0x1FF) + 2000, (resultCode >> 9) & 0x3FFF);
}
private string SystemLanguageToLanguageKey(SystemLanguage systemLanguage)
{
return systemLanguage switch
{
SystemLanguage.Japanese => "ja",
SystemLanguage.AmericanEnglish => "en-US",
SystemLanguage.French => "fr",
SystemLanguage.German => "de",
SystemLanguage.Italian => "it",
SystemLanguage.Spanish => "es",
SystemLanguage.Chinese => "zh-Hans",
SystemLanguage.Korean => "ko",
SystemLanguage.Dutch => "nl",
SystemLanguage.Portuguese => "pt",
SystemLanguage.Russian => "ru",
SystemLanguage.Taiwanese => "zh-HansT",
SystemLanguage.BritishEnglish => "en-GB",
SystemLanguage.CanadianFrench => "fr-CA",
SystemLanguage.LatinAmericanSpanish => "es-419",
SystemLanguage.SimplifiedChinese => "zh-Hans",
SystemLanguage.TraditionalChinese => "zh-Hant",
SystemLanguage.BrazilianPortuguese => "pt-BR",
_ => "en-US"
};
}
private static string CleanText(string value)
{
return CleanTextRegex().Replace(value, "").Replace("\0", "");
}
private string GetMessageText(uint module, uint description, string key)
{
string binaryTitleContentPath = _horizon.ContentManager.GetInstalledContentPath(ErrorMessageBinaryTitleId, StorageId.BuiltInSystem, NcaContentType.Data);
using (LibHac.Fs.IStorage ncaFileStream = new LocalStorage(_horizon.Device.FileSystem.SwitchPathToSystemPath(binaryTitleContentPath), FileAccess.Read, FileMode.Open))
{
Nca nca = new Nca(_horizon.Device.FileSystem.KeySet, ncaFileStream);
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, _horizon.FsIntegrityCheckLevel);
string languageCode = SystemLanguageToLanguageKey(_horizon.State.DesiredSystemLanguage);
string filePath = $"/{module}/{description:0000}/{languageCode}_{key}";
if (romfs.FileExists(filePath))
{
using var binaryFile = new UniqueRef<IFile>();
romfs.OpenFile(ref binaryFile.Ref, filePath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
StreamReader reader = new StreamReader(binaryFile.Get.AsStream(), Encoding.Unicode);
return CleanText(reader.ReadToEnd());
}
else
{
return "";
}
}
}
private string[] GetButtonsText(uint module, uint description, string key)
{
string buttonsText = GetMessageText(module, description, key);
return (buttonsText == "") ? null : buttonsText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
}
private void ParseErrorCommonArg()
{
ErrorCommonArg errorCommonArg = IApplet.ReadStruct<ErrorCommonArg>(_errorStorage);
uint module = errorCommonArg.Module;
uint description = errorCommonArg.Description;
if (_errorCommonHeader.MessageFlag == 0)
{
(module, description) = HexToResultCode(errorCommonArg.ResultCode);
}
string message = GetMessageText(module, description, "DlgMsg");
if (message == "")
{
message = "An error has occured.\n\n"
+ "Please try again later.\n\n"
+ "If the problem persists, please refer to the Ryujinx website.\n"
+ "www.ryujinx.org";
}
string[] buttons = GetButtonsText(module, description, "DlgBtn");
bool showDetails = _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Code: {module}-{description:0000}", "\n" + message, buttons);
if (showDetails)
{
message = GetMessageText(module, description, "FlvMsg");
buttons = GetButtonsText(module, description, "FlvBtn");
_horizon.Device.UiHandler.DisplayErrorAppletDialog($"Details: {module}-{description:0000}", "\n" + message, buttons);
}
}
private void ParseApplicationErrorArg()
{
ApplicationErrorArg applicationErrorArg = IApplet.ReadStruct<ApplicationErrorArg>(_errorStorage);
byte[] messageTextBuffer = new byte[0x800];
byte[] detailsTextBuffer = new byte[0x800];
applicationErrorArg.MessageText.AsSpan().CopyTo(messageTextBuffer);
applicationErrorArg.DetailsText.AsSpan().CopyTo(detailsTextBuffer);
string messageText = Encoding.ASCII.GetString(messageTextBuffer.TakeWhile(b => !b.Equals(0)).ToArray());
string detailsText = Encoding.ASCII.GetString(detailsTextBuffer.TakeWhile(b => !b.Equals(0)).ToArray());
List<string> buttons = new List<string>();
// TODO: Handle the LanguageCode to return the translated "OK" and "Details".
if (detailsText.Trim() != "")
{
buttons.Add("Details");
}
buttons.Add("OK");
bool showDetails = _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber}", "\n" + messageText, buttons.ToArray());
if (showDetails)
{
buttons.RemoveAt(0);
_horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber} (Details)", "\n" + detailsText, buttons.ToArray());
}
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
}
}

View file

@ -0,0 +1,12 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.Error
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ErrorCommonArg
{
public uint Module;
public uint Description;
public uint ResultCode;
}
}

View file

@ -0,0 +1,17 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.Error
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ErrorCommonHeader
{
public ErrorType Type;
public byte JumpFlag;
public byte ReservedFlag1;
public byte ReservedFlag2;
public byte ReservedFlag3;
public byte ContextFlag;
public byte MessageFlag;
public byte ContextFlag2;
}
}

View file

@ -0,0 +1,13 @@
namespace Ryujinx.HLE.HOS.Applets.Error
{
enum ErrorType : byte
{
ErrorCommonArg,
SystemErrorArg,
ApplicationErrorArg,
ErrorEulaArg,
ErrorPctlArg,
ErrorRecordArg,
SystemUpdateEulaArg = 8
}
}

View file

@ -0,0 +1,28 @@
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using Ryujinx.HLE.Ui;
using Ryujinx.Memory;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets
{
interface IApplet
{
event EventHandler AppletStateChanged;
ResultCode Start(AppletSession normalSession,
AppletSession interactiveSession);
ResultCode GetResult();
bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position)
{
return false;
}
static T ReadStruct<T>(ReadOnlySpan<byte> data) where T : unmanaged
{
return MemoryMarshal.Cast<byte, T>(data)[0];
}
}
}

View file

@ -0,0 +1,58 @@
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using System;
using System.IO;
namespace Ryujinx.HLE.HOS.Applets
{
internal class PlayerSelectApplet : IApplet
{
private Horizon _system;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
public event EventHandler AppletStateChanged;
public PlayerSelectApplet(Horizon system)
{
_system = system;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
_normalSession = normalSession;
_interactiveSession = interactiveSession;
// TODO(jduncanator): Parse PlayerSelectConfig from input data
_normalSession.Push(BuildResponse());
AppletStateChanged?.Invoke(this, null);
_system.ReturnFocus();
return ResultCode.Success;
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
private byte[] BuildResponse()
{
UserProfile currentUser = _system.AccountManager.LastOpenedUser;
using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.Write((ulong)PlayerSelectResult.Success);
currentUser.UserId.Write(writer);
return stream.ToArray();
}
}
}
}

View file

@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Applets
{
enum PlayerSelectResult : ulong
{
Success = 0,
Failure = 2
}
}

View file

@ -0,0 +1,18 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Identifies the initial position of the cursor displayed in the area.
/// </summary>
enum InitialCursorPosition : uint
{
/// <summary>
/// Position the cursor at the beginning of the text
/// </summary>
Start,
/// <summary>
/// Position the cursor at the end of the text
/// </summary>
End
}
}

View file

@ -0,0 +1,48 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Possible requests to the software keyboard when running in inline mode.
/// </summary>
enum InlineKeyboardRequest : uint
{
/// <summary>
/// Finalize the keyboard applet.
/// </summary>
Finalize = 0x4,
/// <summary>
/// Set user words for text prediction.
/// </summary>
SetUserWordInfo = 0x6,
/// <summary>
/// Sets the CustomizeDic data. Can't be used if CustomizedDictionaries is already set.
/// </summary>
SetCustomizeDic = 0x7,
/// <summary>
/// Configure the keyboard applet and put it in a state where it is processing input.
/// </summary>
Calc = 0xA,
/// <summary>
/// Set custom dictionaries for text prediction. Can't be used if SetCustomizeDic is already set.
/// </summary>
SetCustomizedDictionaries = 0xB,
/// <summary>
/// Release custom dictionaries data.
/// </summary>
UnsetCustomizedDictionaries = 0xC,
/// <summary>
/// [8.0.0+] Request the keyboard applet to use the ChangedStringV2 response when notifying changes in text data.
/// </summary>
UseChangedStringV2 = 0xD,
/// <summary>
/// [8.0.0+] Request the keyboard applet to use the MovedCursorV2 response when notifying changes in cursor position.
/// </summary>
UseMovedCursorV2 = 0xE
}
}

View file

@ -0,0 +1,93 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Possible responses from the software keyboard when running in inline mode.
/// </summary>
enum InlineKeyboardResponse : uint
{
/// <summary>
/// The software keyboard received a Calc and it is fully initialized. Reply data is ignored by the user-process.
/// </summary>
FinishedInitialize = 0x0,
/// <summary>
/// Default response. Official sw has no handling for this besides just closing the storage.
/// </summary>
Default = 0x1,
/// <summary>
/// The text data in the software keyboard changed (UTF-16 encoding).
/// </summary>
ChangedString = 0x2,
/// <summary>
/// The cursor position in the software keyboard changed (UTF-16 encoding).
/// </summary>
MovedCursor = 0x3,
/// <summary>
/// A tab in the software keyboard changed.
/// </summary>
MovedTab = 0x4,
/// <summary>
/// The OK key was pressed in the software keyboard, confirming the input text (UTF-16 encoding).
/// </summary>
DecidedEnter = 0x5,
/// <summary>
/// The Cancel key was pressed in the software keyboard, cancelling the input.
/// </summary>
DecidedCancel = 0x6,
/// <summary>
/// Same as ChangedString, but with UTF-8 encoding.
/// </summary>
ChangedStringUtf8 = 0x7,
/// <summary>
/// Same as MovedCursor, but with UTF-8 encoding.
/// </summary>
MovedCursorUtf8 = 0x8,
/// <summary>
/// Same as DecidedEnter, but with UTF-8 encoding.
/// </summary>
DecidedEnterUtf8 = 0x9,
/// <summary>
/// They software keyboard is releasing the data previously set by a SetCustomizeDic request.
/// </summary>
UnsetCustomizeDic = 0xA,
/// <summary>
/// They software keyboard is releasing the data previously set by a SetUserWordInfo request.
/// </summary>
ReleasedUserWordInfo = 0xB,
/// <summary>
/// They software keyboard is releasing the data previously set by a SetCustomizedDictionaries request.
/// </summary>
UnsetCustomizedDictionaries = 0xC,
/// <summary>
/// Same as ChangedString, but with additional fields.
/// </summary>
ChangedStringV2 = 0xD,
/// <summary>
/// Same as MovedCursor, but with additional fields.
/// </summary>
MovedCursorV2 = 0xE,
/// <summary>
/// Same as ChangedStringUtf8, but with additional fields.
/// </summary>
ChangedStringUtf8V2 = 0xF,
/// <summary>
/// Same as MovedCursorUtf8, but with additional fields.
/// </summary>
MovedCursorUtf8V2 = 0x10
}
}

View file

@ -0,0 +1,33 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Possible states for the software keyboard when running in inline mode.
/// </summary>
enum InlineKeyboardState : uint
{
/// <summary>
/// The software keyboard has just been created or finalized and is uninitialized.
/// </summary>
Uninitialized = 0x0,
/// <summary>
/// The software keyboard is initialized, but it is not visible and not processing input.
/// </summary>
Initialized = 0x1,
/// <summary>
/// The software keyboard is transitioning to a visible state.
/// </summary>
Appearing = 0x2,
/// <summary>
/// The software keyboard is visible and receiving processing input.
/// </summary>
Shown = 0x3,
/// <summary>
/// software keyboard is transitioning to a hidden state because the user pressed either OK or Cancel.
/// </summary>
Disappearing = 0x4
}
}

View file

@ -0,0 +1,298 @@
using System.IO;
using System.Text;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
internal class InlineResponses
{
private const uint MaxStrLenUTF8 = 0x7D4;
private const uint MaxStrLenUTF16 = 0x3EC;
private static void BeginResponse(InlineKeyboardState state, InlineKeyboardResponse resCode, BinaryWriter writer)
{
writer.Write((uint)state);
writer.Write((uint)resCode);
}
private static uint WriteString(string text, BinaryWriter writer, uint maxSize, Encoding encoding)
{
// Ensure the text fits in the buffer, but do not straight cut the bytes because
// this may corrupt the encoding. Search for a cut in the source string that fits.
byte[] bytes = null;
for (int maxStr = text.Length; maxStr >= 0; maxStr--)
{
// This loop will probably will run only once.
bytes = encoding.GetBytes(text, 0, maxStr);
if (bytes.Length <= maxSize)
{
break;
}
}
writer.Write(bytes);
writer.Seek((int)maxSize - bytes.Length, SeekOrigin.Current);
writer.Write((uint)text.Length); // String size
return (uint)text.Length; // Return the cursor position at the end of the text
}
private static void WriteStringWithCursor(string text, uint cursor, BinaryWriter writer, uint maxSize, Encoding encoding, bool padMiddle)
{
uint length = WriteString(text, writer, maxSize, encoding);
if (cursor > length)
{
cursor = length;
}
if (padMiddle)
{
writer.Write((int)-1); // ?
writer.Write((int)-1); // ?
}
writer.Write(cursor); // Cursor position
}
public static byte[] FinishedInitialize(InlineKeyboardState state)
{
uint resSize = 2 * sizeof(uint) + 0x1;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.FinishedInitialize, writer);
writer.Write((byte)1); // Data (ignored by the program)
return stream.ToArray();
}
}
public static byte[] Default(InlineKeyboardState state)
{
uint resSize = 2 * sizeof(uint);
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.Default, writer);
return stream.ToArray();
}
}
public static byte[] ChangedString(string text, uint cursor, InlineKeyboardState state)
{
uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.ChangedString, writer);
WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, true);
return stream.ToArray();
}
}
public static byte[] MovedCursor(string text, uint cursor, InlineKeyboardState state)
{
uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.MovedCursor, writer);
WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, false);
return stream.ToArray();
}
}
public static byte[] MovedTab(string text, uint cursor, InlineKeyboardState state)
{
// Should be the same as MovedCursor.
uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.MovedTab, writer);
WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, false);
return stream.ToArray();
}
}
public static byte[] DecidedEnter(string text, InlineKeyboardState state)
{
uint resSize = 3 * sizeof(uint) + MaxStrLenUTF16;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.DecidedEnter, writer);
WriteString(text, writer, MaxStrLenUTF16, Encoding.Unicode);
return stream.ToArray();
}
}
public static byte[] DecidedCancel(InlineKeyboardState state)
{
uint resSize = 2 * sizeof(uint);
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.DecidedCancel, writer);
return stream.ToArray();
}
}
public static byte[] ChangedStringUtf8(string text, uint cursor, InlineKeyboardState state)
{
uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8, writer);
WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8, true);
return stream.ToArray();
}
}
public static byte[] MovedCursorUtf8(string text, uint cursor, InlineKeyboardState state)
{
uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8, writer);
WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8, false);
return stream.ToArray();
}
}
public static byte[] DecidedEnterUtf8(string text, InlineKeyboardState state)
{
uint resSize = 3 * sizeof(uint) + MaxStrLenUTF8;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.DecidedEnterUtf8, writer);
WriteString(text, writer, MaxStrLenUTF8, Encoding.UTF8);
return stream.ToArray();
}
}
public static byte[] UnsetCustomizeDic(InlineKeyboardState state)
{
uint resSize = 2 * sizeof(uint);
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.UnsetCustomizeDic, writer);
return stream.ToArray();
}
}
public static byte[] ReleasedUserWordInfo(InlineKeyboardState state)
{
uint resSize = 2 * sizeof(uint);
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.ReleasedUserWordInfo, writer);
return stream.ToArray();
}
}
public static byte[] UnsetCustomizedDictionaries(InlineKeyboardState state)
{
uint resSize = 2 * sizeof(uint);
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.UnsetCustomizedDictionaries, writer);
return stream.ToArray();
}
}
public static byte[] ChangedStringV2(string text, uint cursor, InlineKeyboardState state)
{
uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16 + 0x1;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.ChangedStringV2, writer);
WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, true);
writer.Write((byte)0); // Flag == 0
return stream.ToArray();
}
}
public static byte[] MovedCursorV2(string text, uint cursor, InlineKeyboardState state)
{
uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16 + 0x1;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.MovedCursorV2, writer);
WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, false);
writer.Write((byte)0); // Flag == 0
return stream.ToArray();
}
}
public static byte[] ChangedStringUtf8V2(string text, uint cursor, InlineKeyboardState state)
{
uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8 + 0x1;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8V2, writer);
WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8, true);
writer.Write((byte)0); // Flag == 0
return stream.ToArray();
}
}
public static byte[] MovedCursorUtf8V2(string text, uint cursor, InlineKeyboardState state)
{
uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8 + 0x1;
using (MemoryStream stream = new MemoryStream(new byte[resSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8V2, writer);
WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8, false);
writer.Write((byte)0); // Flag == 0
return stream.ToArray();
}
}
}
}

View file

@ -0,0 +1,18 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Identifies the text entry mode.
/// </summary>
enum InputFormMode : uint
{
/// <summary>
/// Displays the text entry area as a single-line field.
/// </summary>
SingleLine,
/// <summary>
/// Displays the text entry area as a multi-line field.
/// </summary>
MultiLine
}
}

View file

@ -0,0 +1,17 @@
using System;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Identifies prohibited buttons.
/// </summary>
[Flags]
enum InvalidButtonFlags : uint
{
None = 0,
AnalogStickL = 1 << 1,
AnalogStickR = 1 << 2,
ZL = 1 << 3,
ZR = 1 << 4,
}
}

View file

@ -0,0 +1,56 @@
using System;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Identifies prohibited character sets.
/// </summary>
[Flags]
enum InvalidCharFlags : uint
{
/// <summary>
/// No characters are prohibited.
/// </summary>
None = 0 << 1,
/// <summary>
/// Prohibits spaces.
/// </summary>
Space = 1 << 1,
/// <summary>
/// Prohibits the at (@) symbol.
/// </summary>
AtSymbol = 1 << 2,
/// <summary>
/// Prohibits the percent (%) symbol.
/// </summary>
Percent = 1 << 3,
/// <summary>
/// Prohibits the forward slash (/) symbol.
/// </summary>
ForwardSlash = 1 << 4,
/// <summary>
/// Prohibits the backward slash (\) symbol.
/// </summary>
BackSlash = 1 << 5,
/// <summary>
/// Prohibits numbers.
/// </summary>
Numbers = 1 << 6,
/// <summary>
/// Prohibits characters outside of those allowed in download codes.
/// </summary>
DownloadCode = 1 << 7,
/// <summary>
/// Prohibits characters outside of those allowed in Mii Nicknames.
/// </summary>
Username = 1 << 8
}
}

View file

@ -0,0 +1,26 @@
using System;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Bitmask of commands encoded in the Flags field of the Calc structs.
/// </summary>
[Flags]
enum KeyboardCalcFlags : ulong
{
Initialize = 0x1,
SetVolume = 0x2,
Appear = 0x4,
SetInputText = 0x8,
SetCursorPos = 0x10,
SetUtf8Mode = 0x20,
SetKeyboardBackground = 0x100,
SetKeyboardOptions1 = 0x200,
SetKeyboardOptions2 = 0x800,
EnableSeGroup = 0x2000,
DisableSeGroup = 0x4000,
SetBackspaceEnabled = 0x8000,
AppearTrigger = 0x10000,
MustShow = Appear | SetInputText | AppearTrigger
}
}

View file

@ -0,0 +1,14 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Active input options set by the keyboard applet. These options allow keyboard
/// players to input text without conflicting with the controller mappings.
/// </summary>
enum KeyboardInputMode : uint
{
ControllerAndKeyboard,
KeyboardOnly,
ControllerOnly,
Count,
}
}

View file

@ -0,0 +1,12 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// The miniaturization mode used by the keyboard in inline mode.
/// </summary>
enum KeyboardMiniaturizationMode : byte
{
None = 0,
Auto = 1,
Forced = 2
}
}

View file

@ -0,0 +1,31 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Identifies the variant of keyboard displayed on screen.
/// </summary>
enum KeyboardMode : uint
{
/// <summary>
/// A full alpha-numeric keyboard.
/// </summary>
Default = 0,
/// <summary>
/// Number pad.
/// </summary>
NumbersOnly = 1,
/// <summary>
/// ASCII characters keyboard.
/// </summary>
ASCII = 2,
FullLatin = 3,
Alphabet = 4,
SimplifiedChinese = 5,
TraditionalChinese = 6,
Korean = 7,
LanguageSet2 = 8,
LanguageSet2Latin = 9,
}
}

View file

@ -0,0 +1,12 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// The intention of the user when they finish the interaction with the keyboard.
/// </summary>
enum KeyboardResult
{
NotSet = 0,
Accept = 1,
Cancel = 2,
}
}

View file

@ -0,0 +1,18 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Identifies the display mode of text in a password field.
/// </summary>
enum PasswordMode : uint
{
/// <summary>
/// Display input characters.
/// </summary>
Disabled,
/// <summary>
/// Hide input characters.
/// </summary>
Enabled
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\Icon_Accept.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
sodipodi:docname="buttons_ab.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="16.591066"
inkscape:cy="14.090021"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
showguides="false"
inkscape:window-width="1267"
inkscape:window-height="976"
inkscape:window-x="242"
inkscape:window-y="34"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:#ffffff;stroke-width:1.57002;fill-opacity:1"
id="path839"
cx="4.2333331"
cy="4.2333331"
r="4.2333331" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:7.02011px;line-height:1.25;font-family:sans-serif;fill:#4b4b4b;fill-opacity:1;stroke:none;stroke-width:0.376071"
x="1.9222834"
y="6.5921373"
id="text835-2"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\text835-2.png"><tspan
sodipodi:role="line"
id="tspan833-9"
x="1.9222834"
y="6.5921373"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.02011px;font-family:Arial;-inkscape-font-specification:Arial;fill:#4b4b4b;fill-opacity:1;stroke-width:0.376071">A</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

View file

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\Icon_Accept.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
sodipodi:docname="buttons_ab.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="16.591066"
inkscape:cy="14.090021"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
showguides="false"
inkscape:window-width="1267"
inkscape:window-height="976"
inkscape:window-x="242"
inkscape:window-y="34"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:#ffffff;stroke-width:1.57002;fill-opacity:1"
id="path839"
cx="4.2333331"
cy="4.2333331"
r="4.2333331" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:7.02012px;line-height:1.25;font-family:sans-serif;fill:#4b4b4b;fill-opacity:1;stroke:none;stroke-width:0.37607"
x="2.0223334"
y="6.6920195"
id="text835"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><tspan
sodipodi:role="line"
id="tspan833"
x="2.0223334"
y="6.6920195"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.02012px;font-family:Arial;-inkscape-font-specification:Arial;fill:#4b4b4b;fill-opacity:1;stroke-width:0.37607">B</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:7.02011px;line-height:1.25;font-family:sans-serif;fill:#4b4b4b;fill-opacity:1;stroke:none;stroke-width:0.376071"
x="2.0223367"
y="6.6920156"
id="text835-2"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\text835-2.png"><tspan
sodipodi:role="line"
id="tspan833-9"
x="2.0223367"
y="6.6920156"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.02011px;font-family:Arial;-inkscape-font-specification:Arial;fill:#4b4b4b;fill-opacity:1;stroke-width:0.376071">B</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\Icon_KeyF5.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
sodipodi:docname="Icon_KeyF5.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="16.591066"
inkscape:cy="14.090021"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
units="px"
showguides="false"
inkscape:window-width="1267"
inkscape:window-height="976"
inkscape:window-x="242"
inkscape:window-y="25"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#ffffff;stroke-width:2.21199"
id="rect837"
width="8.4666662"
height="8.4666662"
x="1.3877788e-17"
y="0" />
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264578"
x="1.0762799"
y="4.2016153"
id="text835"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
transform="scale(0.9999825,1.0000175)"><tspan
sodipodi:role="line"
id="tspan833"
x="1.0762799"
y="4.2016153"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333px;font-family:Consolas;-inkscape-font-specification:Consolas;stroke-width:0.264578">F6</tspan></text>
<rect
style="fill:none;fill-opacity:1;stroke:#757575;stroke-width:0.26458333;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
id="rect891"
width="6.9844265"
height="6.984426"
x="0.74112016"
y="0.47653681" />
<path
style="fill:none;stroke:#757575;stroke-width:0.264583px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
d="M 0,0 0.74112016,0.47653681"
id="path895"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#757575;stroke-width:0.264583px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
d="M 8.4666662,0 7.7255461,0.47653681"
id="path897"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#757575;stroke-width:0.264583px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
d="M 7.3685303e-7,8.4666667 0.7411209,7.4609628"
id="path901"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#757575;stroke-width:0.264583px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1"
d="M 8.4666669,8.4666667 7.7255468,7.4609628"
id="path903"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -0,0 +1,119 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A structure with appearance configurations for the software keyboard when running in inline mode.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
struct SoftwareKeyboardAppear
{
public const int OkTextLength = SoftwareKeyboardAppearEx.OkTextLength;
public KeyboardMode KeyboardMode;
/// <summary>
/// The string displayed in the Submit button.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = OkTextLength + 1)]
public string OkText;
/// <summary>
/// The character displayed in the left button of the numeric keyboard.
/// </summary>
public char LeftOptionalSymbolKey;
/// <summary>
/// The character displayed in the right button of the numeric keyboard.
/// </summary>
public char RightOptionalSymbolKey;
/// <summary>
/// When set, predictive typing is enabled making use of the system dictionary, and any custom user dictionary.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool PredictionEnabled;
/// <summary>
/// When set, there is only the option to accept the input.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool CancelButtonDisabled;
/// <summary>
/// Specifies prohibited characters that cannot be input into the text entry area.
/// </summary>
public InvalidCharFlags InvalidChars;
/// <summary>
/// Maximum text length allowed.
/// </summary>
public int TextMaxLength;
/// <summary>
/// Minimum text length allowed.
/// </summary>
public int TextMinLength;
/// <summary>
/// Indicates the return button is enabled in the keyboard. This allows for input with multiple lines.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool UseNewLine;
/// <summary>
/// [10.0.0+] If value is 1 or 2, then keytopAsFloating=0 and footerScalable=1 in Calc.
/// </summary>
public KeyboardMiniaturizationMode MiniaturizationMode;
public byte Reserved1;
public byte Reserved2;
/// <summary>
/// Bit field with invalid buttons for the keyboard.
/// </summary>
public InvalidButtonFlags InvalidButtons;
[MarshalAs(UnmanagedType.I1)]
public bool UseSaveData;
public uint Reserved3;
public ushort Reserved4;
public byte Reserved5;
public ulong Reserved6;
public ulong Reserved7;
public SoftwareKeyboardAppearEx ToExtended()
{
SoftwareKeyboardAppearEx appear = new SoftwareKeyboardAppearEx();
appear.KeyboardMode = KeyboardMode;
appear.OkText = OkText;
appear.LeftOptionalSymbolKey = LeftOptionalSymbolKey;
appear.RightOptionalSymbolKey = RightOptionalSymbolKey;
appear.PredictionEnabled = PredictionEnabled;
appear.CancelButtonDisabled = CancelButtonDisabled;
appear.InvalidChars = InvalidChars;
appear.TextMaxLength = TextMaxLength;
appear.TextMinLength = TextMinLength;
appear.UseNewLine = UseNewLine;
appear.MiniaturizationMode = MiniaturizationMode;
appear.Reserved1 = Reserved1;
appear.Reserved2 = Reserved2;
appear.InvalidButtons = InvalidButtons;
appear.UseSaveData = UseSaveData;
appear.Reserved3 = Reserved3;
appear.Reserved4 = Reserved4;
appear.Reserved5 = Reserved5;
appear.Uid0 = Reserved6;
appear.Uid1 = Reserved7;
appear.SamplingNumber = 0;
appear.Reserved6 = 0;
appear.Reserved7 = 0;
appear.Reserved8 = 0;
appear.Reserved9 = 0;
return appear;
}
}
}

View file

@ -0,0 +1,100 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A structure with appearance configurations for the software keyboard when running in inline mode.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
struct SoftwareKeyboardAppearEx
{
public const int OkTextLength = 8;
public KeyboardMode KeyboardMode;
/// <summary>
/// The string displayed in the Submit button.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = OkTextLength + 1)]
public string OkText;
/// <summary>
/// The character displayed in the left button of the numeric keyboard.
/// </summary>
public char LeftOptionalSymbolKey;
/// <summary>
/// The character displayed in the right button of the numeric keyboard.
/// </summary>
public char RightOptionalSymbolKey;
/// <summary>
/// When set, predictive typing is enabled making use of the system dictionary, and any custom user dictionary.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool PredictionEnabled;
/// <summary>
/// When set, there is only the option to accept the input.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool CancelButtonDisabled;
/// <summary>
/// Specifies prohibited characters that cannot be input into the text entry area.
/// </summary>
public InvalidCharFlags InvalidChars;
/// <summary>
/// Maximum text length allowed.
/// </summary>
public int TextMaxLength;
/// <summary>
/// Minimum text length allowed.
/// </summary>
public int TextMinLength;
/// <summary>
/// Indicates the return button is enabled in the keyboard. This allows for input with multiple lines.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool UseNewLine;
/// <summary>
/// [10.0.0+] If value is 1 or 2, then keytopAsFloating=0 and footerScalable=1 in Calc.
/// </summary>
public KeyboardMiniaturizationMode MiniaturizationMode;
public byte Reserved1;
public byte Reserved2;
/// <summary>
/// Bit field with invalid buttons for the keyboard.
/// </summary>
public InvalidButtonFlags InvalidButtons;
[MarshalAs(UnmanagedType.I1)]
public bool UseSaveData;
public uint Reserved3;
public ushort Reserved4;
public byte Reserved5;
/// <summary>
/// The id of the user associated with the appear request.
/// </summary>
public ulong Uid0;
public ulong Uid1;
/// <summary>
/// The sampling number for the keyboard appearance.
/// </summary>
public ulong SamplingNumber;
public ulong Reserved6;
public ulong Reserved7;
public ulong Reserved8;
public ulong Reserved9;
}
}

View file

@ -0,0 +1,816 @@
using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad;
using Ryujinx.HLE.Ui;
using Ryujinx.HLE.Ui.Input;
using Ryujinx.Memory;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.HLE.HOS.Applets
{
internal class SoftwareKeyboardApplet : IApplet
{
private const string DefaultInputText = "Ryujinx";
private const int StandardBufferSize = 0x7D8;
private const int InteractiveBufferSize = 0x7D4;
private const int MaxUserWords = 0x1388;
private const int MaxUiTextSize = 100;
private const Key CycleInputModesKey = Key.F6;
private readonly Switch _device;
private SoftwareKeyboardState _foregroundState = SoftwareKeyboardState.Uninitialized;
private volatile InlineKeyboardState _backgroundState = InlineKeyboardState.Uninitialized;
private bool _isBackground = false;
private AppletSession _normalSession;
private AppletSession _interactiveSession;
// Configuration for foreground mode.
private SoftwareKeyboardConfig _keyboardForegroundConfig;
// Configuration for background (inline) mode.
private SoftwareKeyboardInitialize _keyboardBackgroundInitialize;
private SoftwareKeyboardCustomizeDic _keyboardBackgroundDic;
private SoftwareKeyboardDictSet _keyboardBackgroundDictSet;
private SoftwareKeyboardUserWord[] _keyboardBackgroundUserWords;
private byte[] _transferMemory;
private string _textValue = "";
private int _cursorBegin = 0;
private Encoding _encoding = Encoding.Unicode;
private KeyboardResult _lastResult = KeyboardResult.NotSet;
private IDynamicTextInputHandler _dynamicTextInputHandler = null;
private SoftwareKeyboardRenderer _keyboardRenderer = null;
private NpadReader _npads = null;
private bool _canAcceptController = false;
private KeyboardInputMode _inputMode = KeyboardInputMode.ControllerAndKeyboard;
private object _lock = new object();
public event EventHandler AppletStateChanged;
public SoftwareKeyboardApplet(Horizon system)
{
_device = system.Device;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
lock (_lock)
{
_normalSession = normalSession;
_interactiveSession = interactiveSession;
_interactiveSession.DataAvailable += OnInteractiveData;
var launchParams = _normalSession.Pop();
var keyboardConfig = _normalSession.Pop();
_isBackground = keyboardConfig.Length == Unsafe.SizeOf<SoftwareKeyboardInitialize>();
if (_isBackground)
{
// Initialize the keyboard applet in background mode.
_keyboardBackgroundInitialize = MemoryMarshal.Read<SoftwareKeyboardInitialize>(keyboardConfig);
_backgroundState = InlineKeyboardState.Uninitialized;
if (_device.UiHandler == null)
{
Logger.Error?.Print(LogClass.ServiceAm, "GUI Handler is not set, software keyboard applet will not work properly");
}
else
{
// Create a text handler that converts keyboard strokes to strings.
_dynamicTextInputHandler = _device.UiHandler.CreateDynamicTextInputHandler();
_dynamicTextInputHandler.TextChangedEvent += HandleTextChangedEvent;
_dynamicTextInputHandler.KeyPressedEvent += HandleKeyPressedEvent;
_npads = new NpadReader(_device);
_npads.NpadButtonDownEvent += HandleNpadButtonDownEvent;
_npads.NpadButtonUpEvent += HandleNpadButtonUpEvent;
_keyboardRenderer = new SoftwareKeyboardRenderer(_device.UiHandler.HostUiTheme);
}
return ResultCode.Success;
}
else
{
// Initialize the keyboard applet in foreground mode.
if (keyboardConfig.Length < Marshal.SizeOf<SoftwareKeyboardConfig>())
{
Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}");
}
else
{
_keyboardForegroundConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
}
if (!_normalSession.TryPop(out _transferMemory))
{
Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null");
}
if (_keyboardForegroundConfig.UseUtf8)
{
_encoding = Encoding.UTF8;
}
_foregroundState = SoftwareKeyboardState.Ready;
ExecuteForegroundKeyboard();
return ResultCode.Success;
}
}
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
private bool IsKeyboardActive()
{
return _backgroundState >= InlineKeyboardState.Appearing && _backgroundState < InlineKeyboardState.Disappearing;
}
private bool InputModeControllerEnabled()
{
return _inputMode == KeyboardInputMode.ControllerAndKeyboard ||
_inputMode == KeyboardInputMode.ControllerOnly;
}
private bool InputModeTypingEnabled()
{
return _inputMode == KeyboardInputMode.ControllerAndKeyboard ||
_inputMode == KeyboardInputMode.KeyboardOnly;
}
private void AdvanceInputMode()
{
_inputMode = (KeyboardInputMode)((int)(_inputMode + 1) % (int)KeyboardInputMode.Count);
}
public bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position)
{
_npads?.Update();
_keyboardRenderer?.SetSurfaceInfo(surfaceInfo);
return _keyboardRenderer?.DrawTo(destination, position) ?? false;
}
private void ExecuteForegroundKeyboard()
{
string initialText = null;
// Initial Text is always encoded as a UTF-16 string in the work buffer (passed as transfer memory)
// InitialStringOffset points to the memory offset and InitialStringLength is the number of UTF-16 characters
if (_transferMemory != null && _keyboardForegroundConfig.InitialStringLength > 0)
{
initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardForegroundConfig.InitialStringOffset,
2 * _keyboardForegroundConfig.InitialStringLength);
}
// If the max string length is 0, we set it to a large default
// length.
if (_keyboardForegroundConfig.StringLengthMax == 0)
{
_keyboardForegroundConfig.StringLengthMax = 100;
}
if (_device.UiHandler == null)
{
Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default");
_textValue = DefaultInputText;
_lastResult = KeyboardResult.Accept;
}
else
{
// Call the configured GUI handler to get user's input.
var args = new SoftwareKeyboardUiArgs
{
HeaderText = StripUnicodeControlCodes(_keyboardForegroundConfig.HeaderText),
SubtitleText = StripUnicodeControlCodes(_keyboardForegroundConfig.SubtitleText),
GuideText = StripUnicodeControlCodes(_keyboardForegroundConfig.GuideText),
SubmitText = (!string.IsNullOrWhiteSpace(_keyboardForegroundConfig.SubmitText) ?
_keyboardForegroundConfig.SubmitText : "OK"),
StringLengthMin = _keyboardForegroundConfig.StringLengthMin,
StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
InitialText = initialText
};
_lastResult = _device.UiHandler.DisplayInputDialog(args, out _textValue) ? KeyboardResult.Accept : KeyboardResult.Cancel;
_textValue ??= initialText ?? DefaultInputText;
}
// If the game requests a string with a minimum length less
// than our default text, repeat our default text until we meet
// the minimum length requirement.
// This should always be done before the text truncation step.
while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin)
{
_textValue = String.Join(" ", _textValue, _textValue);
}
// If our default text is longer than the allowed length,
// we truncate it.
if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax)
{
_textValue = _textValue.Substring(0, _keyboardForegroundConfig.StringLengthMax);
}
// Does the application want to validate the text itself?
if (_keyboardForegroundConfig.CheckText)
{
// 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.
_foregroundState = SoftwareKeyboardState.ValidationPending;
PushForegroundResponse(true);
}
else
{
// 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.
_foregroundState = SoftwareKeyboardState.Complete;
PushForegroundResponse(false);
AppletStateChanged?.Invoke(this, null);
}
}
private void OnInteractiveData(object sender, EventArgs e)
{
// Obtain the validation status response.
var data = _interactiveSession.Pop();
if (_isBackground)
{
lock (_lock)
{
OnBackgroundInteractiveData(data);
}
}
else
{
OnForegroundInteractiveData(data);
}
}
private void OnForegroundInteractiveData(byte[] data)
{
if (_foregroundState == 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.
PushForegroundResponse(false);
AppletStateChanged?.Invoke(this, null);
_foregroundState = SoftwareKeyboardState.Complete;
}
else if (_foregroundState == SoftwareKeyboardState.Complete)
{
// If we have already completed, we push the result text
// back on the output buffer and poll the application.
PushForegroundResponse(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 void OnBackgroundInteractiveData(byte[] data)
{
// WARNING: Only invoke applet state changes after an explicit finalization
// request from the game, this is because the inline keyboard is expected to
// keep running in the background sending data by itself.
using (MemoryStream stream = new MemoryStream(data))
using (BinaryReader reader = new BinaryReader(stream))
{
var request = (InlineKeyboardRequest)reader.ReadUInt32();
long remaining;
Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {_backgroundState}");
switch (request)
{
case InlineKeyboardRequest.UseChangedStringV2:
Logger.Stub?.Print(LogClass.ServiceAm, "Inline keyboard request UseChangedStringV2");
break;
case InlineKeyboardRequest.UseMovedCursorV2:
Logger.Stub?.Print(LogClass.ServiceAm, "Inline keyboard request UseMovedCursorV2");
break;
case InlineKeyboardRequest.SetUserWordInfo:
// Read the user word info data.
remaining = stream.Length - stream.Position;
if (remaining < sizeof(int))
{
Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes");
}
else
{
int wordsCount = reader.ReadInt32();
int wordSize = Unsafe.SizeOf<SoftwareKeyboardUserWord>();
remaining = stream.Length - stream.Position;
if (wordsCount > MaxUserWords)
{
Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}");
}
else if (wordsCount * wordSize != remaining)
{
Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words");
}
else
{
_keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount];
for (int word = 0; word < wordsCount; word++)
{
_keyboardBackgroundUserWords[word] = reader.ReadStruct<SoftwareKeyboardUserWord>();
}
}
}
_interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(_backgroundState));
break;
case InlineKeyboardRequest.SetCustomizeDic:
// Read the custom dic data.
remaining = stream.Length - stream.Position;
if (remaining != Unsafe.SizeOf<SoftwareKeyboardCustomizeDic>())
{
Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes");
}
else
{
_keyboardBackgroundDic = reader.ReadStruct<SoftwareKeyboardCustomizeDic>();
}
break;
case InlineKeyboardRequest.SetCustomizedDictionaries:
// Read the custom dictionaries data.
remaining = stream.Length - stream.Position;
if (remaining != Unsafe.SizeOf<SoftwareKeyboardDictSet>())
{
Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes");
}
else
{
_keyboardBackgroundDictSet = reader.ReadStruct<SoftwareKeyboardDictSet>();
}
break;
case InlineKeyboardRequest.Calc:
// The Calc request is used to communicate configuration changes and commands to the keyboard.
// Fields in the Calc struct and operations are masked by the Flags field.
// Read the Calc data.
SoftwareKeyboardCalcEx newCalc;
remaining = stream.Length - stream.Position;
if (remaining == Marshal.SizeOf<SoftwareKeyboardCalc>())
{
var keyboardCalcData = reader.ReadBytes((int)remaining);
var keyboardCalc = ReadStruct<SoftwareKeyboardCalc>(keyboardCalcData);
newCalc = keyboardCalc.ToExtended();
}
else if (remaining == Marshal.SizeOf<SoftwareKeyboardCalcEx>() || remaining == SoftwareKeyboardCalcEx.AlternativeSize)
{
var keyboardCalcData = reader.ReadBytes((int)remaining);
newCalc = ReadStruct<SoftwareKeyboardCalcEx>(keyboardCalcData);
}
else
{
Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes");
newCalc = new SoftwareKeyboardCalcEx();
}
// Process each individual operation specified in the flags.
bool updateText = false;
if ((newCalc.Flags & KeyboardCalcFlags.Initialize) != 0)
{
_interactiveSession.Push(InlineResponses.FinishedInitialize(_backgroundState));
_backgroundState = InlineKeyboardState.Initialized;
}
if ((newCalc.Flags & KeyboardCalcFlags.SetCursorPos) != 0)
{
_cursorBegin = newCalc.CursorPos;
updateText = true;
Logger.Debug?.Print(LogClass.ServiceAm, $"Cursor position set to {_cursorBegin}");
}
if ((newCalc.Flags & KeyboardCalcFlags.SetInputText) != 0)
{
_textValue = newCalc.InputText;
updateText = true;
Logger.Debug?.Print(LogClass.ServiceAm, $"Input text set to {_textValue}");
}
if ((newCalc.Flags & KeyboardCalcFlags.SetUtf8Mode) != 0)
{
_encoding = newCalc.UseUtf8 ? Encoding.UTF8 : Encoding.Default;
Logger.Debug?.Print(LogClass.ServiceAm, $"Encoding set to {_encoding}");
}
if (updateText)
{
_dynamicTextInputHandler.SetText(_textValue, _cursorBegin);
_keyboardRenderer.UpdateTextState(_textValue, _cursorBegin, _cursorBegin, null, null);
}
if ((newCalc.Flags & KeyboardCalcFlags.MustShow) != 0)
{
ActivateFrontend();
_backgroundState = InlineKeyboardState.Shown;
PushChangedString(_textValue, (uint)_cursorBegin, _backgroundState);
}
// Send the response to the Calc
_interactiveSession.Push(InlineResponses.Default(_backgroundState));
break;
case InlineKeyboardRequest.Finalize:
// Destroy the frontend.
DestroyFrontend();
// The calling application wants to close the keyboard applet and will wait for a state change.
_backgroundState = InlineKeyboardState.Uninitialized;
AppletStateChanged?.Invoke(this, null);
break;
default:
// We shouldn't be able to get here through standard swkbd execution.
Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_backgroundState}");
_interactiveSession.Push(InlineResponses.Default(_backgroundState));
break;
}
}
}
private void ActivateFrontend()
{
Logger.Debug?.Print(LogClass.ServiceAm, $"Activating software keyboard frontend");
_inputMode = KeyboardInputMode.ControllerAndKeyboard;
_npads.Update(true);
NpadButton buttons = _npads.GetCurrentButtonsOfAllNpads();
// Block the input if the current accept key is pressed so the applet won't be instantly closed.
_canAcceptController = (buttons & NpadButton.A) == 0;
_dynamicTextInputHandler.TextProcessingEnabled = true;
_keyboardRenderer.UpdateCommandState(null, null, true);
_keyboardRenderer.UpdateTextState(null, null, null, null, true);
}
private void DeactivateFrontend()
{
Logger.Debug?.Print(LogClass.ServiceAm, $"Deactivating software keyboard frontend");
_inputMode = KeyboardInputMode.ControllerAndKeyboard;
_canAcceptController = false;
_dynamicTextInputHandler.TextProcessingEnabled = false;
_dynamicTextInputHandler.SetText(_textValue, _cursorBegin);
}
private void DestroyFrontend()
{
Logger.Debug?.Print(LogClass.ServiceAm, $"Destroying software keyboard frontend");
_keyboardRenderer?.Dispose();
_keyboardRenderer = null;
if (_dynamicTextInputHandler != null)
{
_dynamicTextInputHandler.TextChangedEvent -= HandleTextChangedEvent;
_dynamicTextInputHandler.KeyPressedEvent -= HandleKeyPressedEvent;
_dynamicTextInputHandler.Dispose();
_dynamicTextInputHandler = null;
}
if (_npads != null)
{
_npads.NpadButtonDownEvent -= HandleNpadButtonDownEvent;
_npads.NpadButtonUpEvent -= HandleNpadButtonUpEvent;
_npads = null;
}
}
private bool HandleKeyPressedEvent(Key key)
{
if (key == CycleInputModesKey)
{
lock (_lock)
{
if (IsKeyboardActive())
{
AdvanceInputMode();
bool typingEnabled = InputModeTypingEnabled();
bool controllerEnabled = InputModeControllerEnabled();
_dynamicTextInputHandler.TextProcessingEnabled = typingEnabled;
_keyboardRenderer.UpdateTextState(null, null, null, null, typingEnabled);
_keyboardRenderer.UpdateCommandState(null, null, controllerEnabled);
}
}
}
return true;
}
private void HandleTextChangedEvent(string text, int cursorBegin, int cursorEnd, bool overwriteMode)
{
lock (_lock)
{
// Text processing should not run with typing disabled.
Debug.Assert(InputModeTypingEnabled());
if (text.Length > MaxUiTextSize)
{
// Limit the text size and change it back.
text = text.Substring(0, MaxUiTextSize);
cursorBegin = Math.Min(cursorBegin, MaxUiTextSize);
cursorEnd = Math.Min(cursorEnd, MaxUiTextSize);
_dynamicTextInputHandler.SetText(text, cursorBegin, cursorEnd);
}
_textValue = text;
_cursorBegin = cursorBegin;
_keyboardRenderer.UpdateTextState(text, cursorBegin, cursorEnd, overwriteMode, null);
PushUpdatedState(text, cursorBegin, KeyboardResult.NotSet);
}
}
private void HandleNpadButtonDownEvent(int npadIndex, NpadButton button)
{
lock (_lock)
{
if (!IsKeyboardActive())
{
return;
}
switch (button)
{
case NpadButton.A:
_keyboardRenderer.UpdateCommandState(_canAcceptController, null, null);
break;
case NpadButton.B:
_keyboardRenderer.UpdateCommandState(null, _canAcceptController, null);
break;
}
}
}
private void HandleNpadButtonUpEvent(int npadIndex, NpadButton button)
{
lock (_lock)
{
KeyboardResult result = KeyboardResult.NotSet;
switch (button)
{
case NpadButton.A:
result = KeyboardResult.Accept;
_keyboardRenderer.UpdateCommandState(false, null, null);
break;
case NpadButton.B:
result = KeyboardResult.Cancel;
_keyboardRenderer.UpdateCommandState(null, false, null);
break;
}
if (IsKeyboardActive())
{
if (!_canAcceptController)
{
_canAcceptController = true;
}
else if (InputModeControllerEnabled())
{
PushUpdatedState(_textValue, _cursorBegin, result);
}
}
}
}
private void PushUpdatedState(string text, int cursorBegin, KeyboardResult result)
{
_lastResult = result;
_textValue = text;
bool cancel = result == KeyboardResult.Cancel;
bool accept = result == KeyboardResult.Accept;
if (!IsKeyboardActive())
{
// Keyboard is not active.
return;
}
if (accept == false && cancel == false)
{
Logger.Debug?.Print(LogClass.ServiceAm, $"Updating keyboard text to {text} and cursor position to {cursorBegin}");
PushChangedString(text, (uint)cursorBegin, _backgroundState);
}
else
{
// Disable the frontend.
DeactivateFrontend();
// The 'Complete' state indicates the Calc request has been fulfilled by the applet.
_backgroundState = InlineKeyboardState.Disappearing;
if (accept)
{
Logger.Debug?.Print(LogClass.ServiceAm, $"Sending keyboard OK with text {text}");
DecidedEnter(text, _backgroundState);
}
else if (cancel)
{
Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel");
DecidedCancel(_backgroundState);
}
_interactiveSession.Push(InlineResponses.Default(_backgroundState));
Logger.Debug?.Print(LogClass.ServiceAm, $"Resetting state of the keyboard to {_backgroundState}");
// Set the state of the applet to 'Initialized' as it is the only known state so far
// that does not soft-lock the keyboard after use.
_backgroundState = InlineKeyboardState.Initialized;
_interactiveSession.Push(InlineResponses.Default(_backgroundState));
}
}
private void PushChangedString(string text, uint cursor, InlineKeyboardState state)
{
// TODO (Caian): The *V2 methods are not supported because the applications that request
// them do not seem to accept them. The regular methods seem to work just fine in all cases.
if (_encoding == Encoding.UTF8)
{
_interactiveSession.Push(InlineResponses.ChangedStringUtf8(text, cursor, state));
}
else
{
_interactiveSession.Push(InlineResponses.ChangedString(text, cursor, state));
}
}
private void DecidedEnter(string text, InlineKeyboardState state)
{
if (_encoding == Encoding.UTF8)
{
_interactiveSession.Push(InlineResponses.DecidedEnterUtf8(text, state));
}
else
{
_interactiveSession.Push(InlineResponses.DecidedEnter(text, state));
}
}
private void DecidedCancel(InlineKeyboardState state)
{
_interactiveSession.Push(InlineResponses.DecidedCancel(state));
}
private void PushForegroundResponse(bool interactive)
{
int bufferSize = interactive ? InteractiveBufferSize : StandardBufferSize;
using (MemoryStream stream = new MemoryStream(new byte[bufferSize]))
using (BinaryWriter writer = new BinaryWriter(stream))
{
byte[] output = _encoding.GetBytes(_textValue);
if (!interactive)
{
// Result Code.
writer.Write(_lastResult == KeyboardResult.Accept ? 0U : 1U);
}
else
{
// In interactive mode, we write the length of the text as a long, rather than
// a result code. This field is inclusive of the 64-bit size.
writer.Write((long)output.Length + 8);
}
writer.Write(output);
if (!interactive)
{
_normalSession.Push(stream.ToArray());
}
else
{
_interactiveSession.Push(stream.ToArray());
}
}
}
/// <summary>
/// Removes all Unicode control code characters from the input string.
/// This includes CR/LF, tabs, null characters, escape characters,
/// and special control codes which are used for formatting by the real keyboard applet.
/// </summary>
/// <remarks>
/// Some games send special control codes (such as 0x13 "Device Control 3") as part of the string.
/// Future implementations of the emulated keyboard applet will need to handle these as well.
/// </remarks>
/// <param name="input">The input string to sanitize (may be null).</param>
/// <returns>The sanitized string.</returns>
internal static string StripUnicodeControlCodes(string input)
{
if (input is null)
{
return null;
}
if (input.Length == 0)
{
return string.Empty;
}
StringBuilder sb = new StringBuilder(capacity: input.Length);
foreach (char c in input)
{
if (!char.IsControl(c))
{
sb.Append(c);
}
}
return sb.ToString();
}
private static T ReadStruct<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] 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,220 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A structure with configuration options of the software keyboard when starting a new input request in inline mode.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
struct SoftwareKeyboardCalc
{
public const int InputTextLength = SoftwareKeyboardCalcEx.InputTextLength;
public uint Unknown;
/// <summary>
/// The size of the Calc struct, as reported by the process communicating with the applet.
/// </summary>
public ushort Size;
public byte Unknown1;
public byte Unknown2;
/// <summary>
/// Configuration flags. Each bit in the bitfield enabled a different operation of the keyboard
/// using the data provided with the Calc structure.
/// </summary>
public KeyboardCalcFlags Flags;
/// <summary>
/// The original parameters used when initializing the keyboard applet.
/// Flag: 0x1
/// </summary>
public SoftwareKeyboardInitialize Initialize;
/// <summary>
/// The audio volume used by the sound effects of the keyboard.
/// Flag: 0x2
/// </summary>
public float Volume;
/// <summary>
/// The initial position of the text cursor (caret) in the provided input text.
/// Flag: 0x10
/// </summary>
public int CursorPos;
/// <summary>
/// Appearance configurations for the on-screen keyboard.
/// </summary>
public SoftwareKeyboardAppear Appear;
/// <summary>
/// The initial input text to be used by the software keyboard.
/// Flag: 0x8
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = InputTextLength + 1)]
public string InputText;
/// <summary>
/// When set, the strings communicated by software keyboard will be encoded as UTF-8 instead of UTF-16.
/// Flag: 0x20
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool UseUtf8;
public byte Unknown3;
/// <summary>
/// [5.0.0+] Enable the backspace key in the software keyboard.
/// Flag: 0x8000
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool BackspaceEnabled;
public short Unknown4;
public byte Unknown5;
/// <summary>
/// Flag: 0x200
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool KeytopAsFloating;
/// <summary>
/// Flag: 0x100
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool FooterScalable;
/// <summary>
/// Flag: 0x100
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool AlphaEnabledInInputMode;
/// <summary>
/// Flag: 0x100
/// </summary>
public byte InputModeFadeType;
/// <summary>
/// When set, the software keyboard ignores touch input.
/// Flag: 0x200
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool TouchDisabled;
/// <summary>
/// When set, the software keyboard ignores hardware keyboard commands.
/// Flag: 0x800
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool HardwareKeyboardDisabled;
public uint Unknown6;
public uint Unknown7;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x200
/// </summary>
public float KeytopScale0;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x200
/// </summary>
public float KeytopScale1;
public float KeytopTranslate0;
public float KeytopTranslate1;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x100
/// </summary>
public float KeytopBgAlpha;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x100
/// </summary>
public float FooterBgAlpha;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x200
/// </summary>
public float BalloonScale;
public float Unknown8;
public uint Unknown9;
public uint Unknown10;
public uint Unknown11;
/// <summary>
/// [5.0.0+] Enable sound effect.
/// Flag: Enable: 0x2000
/// Disable: 0x4000
/// </summary>
public byte SeGroup;
/// <summary>
/// [6.0.0+] Enables the Trigger field when Trigger is non-zero.
/// </summary>
public byte TriggerFlag;
/// <summary>
/// [6.0.0+] Always set to zero.
/// </summary>
public byte Trigger;
public byte Padding;
public SoftwareKeyboardCalcEx ToExtended()
{
SoftwareKeyboardCalcEx calc = new SoftwareKeyboardCalcEx();
calc.Unknown = Unknown;
calc.Size = Size;
calc.Unknown1 = Unknown1;
calc.Unknown2 = Unknown2;
calc.Flags = Flags;
calc.Initialize = Initialize;
calc.Volume = Volume;
calc.CursorPos = CursorPos;
calc.Appear = Appear.ToExtended();
calc.InputText = InputText;
calc.UseUtf8 = UseUtf8;
calc.Unknown3 = Unknown3;
calc.BackspaceEnabled = BackspaceEnabled;
calc.Unknown4 = Unknown4;
calc.Unknown5 = Unknown5;
calc.KeytopAsFloating = KeytopAsFloating;
calc.FooterScalable = FooterScalable;
calc.AlphaEnabledInInputMode = AlphaEnabledInInputMode;
calc.InputModeFadeType = InputModeFadeType;
calc.TouchDisabled = TouchDisabled;
calc.HardwareKeyboardDisabled = HardwareKeyboardDisabled;
calc.Unknown6 = Unknown6;
calc.Unknown7 = Unknown7;
calc.KeytopScale0 = KeytopScale0;
calc.KeytopScale1 = KeytopScale1;
calc.KeytopTranslate0 = KeytopTranslate0;
calc.KeytopTranslate1 = KeytopTranslate1;
calc.KeytopBgAlpha = KeytopBgAlpha;
calc.FooterBgAlpha = FooterBgAlpha;
calc.BalloonScale = BalloonScale;
calc.Unknown8 = Unknown8;
calc.Unknown9 = Unknown9;
calc.Unknown10 = Unknown10;
calc.Unknown11 = Unknown11;
calc.SeGroup = SeGroup;
calc.TriggerFlag = TriggerFlag;
calc.Trigger = Trigger;
return calc;
}
}
}

View file

@ -0,0 +1,182 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A structure with configuration options of the software keyboard when starting a new input request in inline mode.
/// This is the extended version of the structure with extended appear options.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
struct SoftwareKeyboardCalcEx
{
/// <summary>
/// This struct was built following Switchbrew's specs, but this size (larger) is also found in real games.
/// It's assumed that this is padding at the end of this struct, because all members seem OK.
/// </summary>
public const int AlternativeSize = 1256;
public const int InputTextLength = 505;
public uint Unknown;
/// <summary>
/// The size of the Calc struct, as reported by the process communicating with the applet.
/// </summary>
public ushort Size;
public byte Unknown1;
public byte Unknown2;
/// <summary>
/// Configuration flags. Each bit in the bitfield enabled a different operation of the keyboard
/// using the data provided with the Calc structure.
/// </summary>
public KeyboardCalcFlags Flags;
/// <summary>
/// The original parameters used when initializing the keyboard applet.
/// Flag: 0x1
/// </summary>
public SoftwareKeyboardInitialize Initialize;
/// <summary>
/// The audio volume used by the sound effects of the keyboard.
/// Flag: 0x2
/// </summary>
public float Volume;
/// <summary>
/// The initial position of the text cursor (caret) in the provided input text.
/// Flag: 0x10
/// </summary>
public int CursorPos;
/// <summary>
/// Appearance configurations for the on-screen keyboard.
/// </summary>
public SoftwareKeyboardAppearEx Appear;
/// <summary>
/// The initial input text to be used by the software keyboard.
/// Flag: 0x8
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = InputTextLength + 1)]
public string InputText;
/// <summary>
/// When set, the strings communicated by software keyboard will be encoded as UTF-8 instead of UTF-16.
/// Flag: 0x20
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool UseUtf8;
public byte Unknown3;
/// <summary>
/// [5.0.0+] Enable the backspace key in the software keyboard.
/// Flag: 0x8000
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool BackspaceEnabled;
public short Unknown4;
public byte Unknown5;
/// <summary>
/// Flag: 0x200
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool KeytopAsFloating;
/// <summary>
/// Flag: 0x100
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool FooterScalable;
/// <summary>
/// Flag: 0x100
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool AlphaEnabledInInputMode;
/// <summary>
/// Flag: 0x100
/// </summary>
public byte InputModeFadeType;
/// <summary>
/// When set, the software keyboard ignores touch input.
/// Flag: 0x200
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool TouchDisabled;
/// <summary>
/// When set, the software keyboard ignores hardware keyboard commands.
/// Flag: 0x800
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool HardwareKeyboardDisabled;
public uint Unknown6;
public uint Unknown7;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x200
/// </summary>
public float KeytopScale0;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x200
/// </summary>
public float KeytopScale1;
public float KeytopTranslate0;
public float KeytopTranslate1;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x100
/// </summary>
public float KeytopBgAlpha;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x100
/// </summary>
public float FooterBgAlpha;
/// <summary>
/// Default value is 1.0.
/// Flag: 0x200
/// </summary>
public float BalloonScale;
public float Unknown8;
public uint Unknown9;
public uint Unknown10;
public uint Unknown11;
/// <summary>
/// [5.0.0+] Enable sound effect.
/// Flag: Enable: 0x2000
/// Disable: 0x4000
/// </summary>
public byte SeGroup;
/// <summary>
/// [6.0.0+] Enables the Trigger field when Trigger is non-zero.
/// </summary>
public byte TriggerFlag;
/// <summary>
/// [6.0.0+] Always set to zero.
/// </summary>
public byte Trigger;
public byte Padding;
}
}

View file

@ -0,0 +1,138 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A structure that defines the configuration options of the software keyboard.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct SoftwareKeyboardConfig
{
private const int SubmitTextLength = 8;
private const int HeaderTextLength = 64;
private const int SubtitleTextLength = 128;
private const int GuideTextLength = 256;
/// <summary>
/// Type of keyboard.
/// </summary>
public KeyboardMode Mode;
/// <summary>
/// The string displayed in the Submit button.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = SubmitTextLength + 1)]
public string SubmitText;
/// <summary>
/// The character displayed in the left button of the numeric keyboard.
/// This is ignored when Mode is not set to NumbersOnly.
/// </summary>
public char LeftOptionalSymbolKey;
/// <summary>
/// The character displayed in the right button of the numeric keyboard.
/// This is ignored when Mode is not set to NumbersOnly.
/// </summary>
public char RightOptionalSymbolKey;
/// <summary>
/// When set, predictive typing is enabled making use of the system dictionary,
/// and any custom user dictionary.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool PredictionEnabled;
/// <summary>
/// Specifies prohibited characters that cannot be input into the text entry area.
/// </summary>
public InvalidCharFlags InvalidCharFlag;
/// <summary>
/// The initial position of the text cursor displayed in the text entry area.
/// </summary>
public InitialCursorPosition InitialCursorPosition;
/// <summary>
/// The string displayed in the header area of the keyboard.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = HeaderTextLength + 1)]
public string HeaderText;
/// <summary>
/// The string displayed in the subtitle area of the keyboard.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = SubtitleTextLength + 1)]
public string SubtitleText;
/// <summary>
/// The placeholder string displayed in the text entry area when no text is entered.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = GuideTextLength + 1)]
public string GuideText;
/// <summary>
/// When non-zero, specifies the maximum allowed length of the string entered into the text entry area.
/// </summary>
public int StringLengthMax;
/// <summary>
/// When non-zero, specifies the minimum allowed length of the string entered into the text entry area.
/// </summary>
public int StringLengthMin;
/// <summary>
/// When enabled, hides input characters as dots in the text entry area.
/// </summary>
public PasswordMode PasswordMode;
/// <summary>
/// Specifies whether the text entry area is displayed as a single-line entry, or a multi-line entry field.
/// </summary>
public InputFormMode InputFormMode;
/// <summary>
/// When set, enables or disables the return key. This value is ignored when single-line entry is specified as the InputFormMode.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool UseNewLine;
/// <summary>
/// When set, the software keyboard will return a UTF-8 encoded string, rather than UTF-16.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool UseUtf8;
/// <summary>
/// When set, the software keyboard will blur the game application rendered behind the keyboard.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool UseBlurBackground;
/// <summary>
/// Offset into the work buffer of the initial text when the keyboard is first displayed.
/// </summary>
public int InitialStringOffset;
/// <summary>
/// Length of the initial text.
/// </summary>
public int InitialStringLength;
/// <summary>
/// Offset into the work buffer of the custom user dictionary.
/// </summary>
public int CustomDictionaryOffset;
/// <summary>
/// Number of entries in the custom user dictionary.
/// </summary>
public int CustomDictionaryCount;
/// <summary>
/// When set, the text entered will be validated on the application side after the keyboard has been submitted.
/// </summary>
[MarshalAs(UnmanagedType.I1)]
public bool CheckText;
}
}

View file

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A structure used by SetCustomizeDic request to software keyboard.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x70)]
struct SoftwareKeyboardCustomizeDic
{
// Unknown
}
}

View file

@ -0,0 +1,34 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A structure with custom dictionary words for the software keyboard.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 2)]
struct SoftwareKeyboardDictSet
{
/// <summary>
/// A 0x1000-byte aligned buffer position.
/// </summary>
public ulong BufferPosition;
/// <summary>
/// A 0x1000-byte aligned buffer size.
/// </summary>
public uint BufferSize;
/// <summary>
/// Array of word entries in the buffer.
/// </summary>
public Array24<ulong> Entries;
/// <summary>
/// Number of used entries in the Entries field.
/// </summary>
public ushort TotalEntries;
public ushort Padding1;
}
}

View file

@ -0,0 +1,26 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A structure that mirrors the parameters used to initialize the keyboard applet.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct SoftwareKeyboardInitialize
{
public uint Unknown;
/// <summary>
/// The applet mode used when launching the swkb. The bits regarding the background vs foreground mode can be wrong.
/// </summary>
public byte LibMode;
/// <summary>
/// [5.0.0+] Set to 0x1 to indicate a firmware version >= 5.0.0.
/// </summary>
public byte FivePlus;
public byte Padding1;
public byte Padding2;
}
}

View file

@ -0,0 +1,164 @@
using Ryujinx.HLE.Ui;
using Ryujinx.Memory;
using System;
using System.Threading;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Class that manages the renderer base class and its state in a multithreaded context.
/// </summary>
internal class SoftwareKeyboardRenderer : IDisposable
{
private const int TextBoxBlinkSleepMilliseconds = 100;
private const int RendererWaitTimeoutMilliseconds = 100;
private readonly object _stateLock = new object();
private SoftwareKeyboardUiState _state = new SoftwareKeyboardUiState();
private SoftwareKeyboardRendererBase _renderer;
private TimedAction _textBoxBlinkTimedAction = new TimedAction();
private TimedAction _renderAction = new TimedAction();
public SoftwareKeyboardRenderer(IHostUiTheme uiTheme)
{
_renderer = new SoftwareKeyboardRendererBase(uiTheme);
StartTextBoxBlinker(_textBoxBlinkTimedAction, _state, _stateLock);
StartRenderer(_renderAction, _renderer, _state, _stateLock);
}
private static void StartTextBoxBlinker(TimedAction timedAction, SoftwareKeyboardUiState state, object stateLock)
{
timedAction.Reset(() =>
{
lock (stateLock)
{
// The blinker is on half of the time and events such as input
// changes can reset the blinker.
state.TextBoxBlinkCounter = (state.TextBoxBlinkCounter + 1) % (2 * SoftwareKeyboardRendererBase.TextBoxBlinkThreshold);
// Tell the render thread there is something new to render.
Monitor.PulseAll(stateLock);
}
}, TextBoxBlinkSleepMilliseconds);
}
private static void StartRenderer(TimedAction timedAction, SoftwareKeyboardRendererBase renderer, SoftwareKeyboardUiState state, object stateLock)
{
SoftwareKeyboardUiState internalState = new SoftwareKeyboardUiState();
bool canCreateSurface = false;
bool needsUpdate = true;
timedAction.Reset(() =>
{
lock (stateLock)
{
if (!Monitor.Wait(stateLock, RendererWaitTimeoutMilliseconds))
{
return;
}
needsUpdate = UpdateStateField(ref state.InputText, ref internalState.InputText);
needsUpdate |= UpdateStateField(ref state.CursorBegin, ref internalState.CursorBegin);
needsUpdate |= UpdateStateField(ref state.CursorEnd, ref internalState.CursorEnd);
needsUpdate |= UpdateStateField(ref state.AcceptPressed, ref internalState.AcceptPressed);
needsUpdate |= UpdateStateField(ref state.CancelPressed, ref internalState.CancelPressed);
needsUpdate |= UpdateStateField(ref state.OverwriteMode, ref internalState.OverwriteMode);
needsUpdate |= UpdateStateField(ref state.TypingEnabled, ref internalState.TypingEnabled);
needsUpdate |= UpdateStateField(ref state.ControllerEnabled, ref internalState.ControllerEnabled);
needsUpdate |= UpdateStateField(ref state.TextBoxBlinkCounter, ref internalState.TextBoxBlinkCounter);
canCreateSurface = state.SurfaceInfo != null && internalState.SurfaceInfo == null;
if (canCreateSurface)
{
internalState.SurfaceInfo = state.SurfaceInfo;
}
}
if (canCreateSurface)
{
renderer.CreateSurface(internalState.SurfaceInfo);
}
if (needsUpdate)
{
renderer.DrawMutableElements(internalState);
renderer.CopyImageToBuffer();
needsUpdate = false;
}
});
}
private static bool UpdateStateField<T>(ref T source, ref T destination) where T : IEquatable<T>
{
if (!source.Equals(destination))
{
destination = source;
return true;
}
return false;
}
#pragma warning disable CS8632
public void UpdateTextState(string? inputText, int? cursorBegin, int? cursorEnd, bool? overwriteMode, bool? typingEnabled)
#pragma warning restore CS8632
{
lock (_stateLock)
{
// Update the parameters that were provided.
_state.InputText = inputText != null ? inputText : _state.InputText;
_state.CursorBegin = cursorBegin.GetValueOrDefault(_state.CursorBegin);
_state.CursorEnd = cursorEnd.GetValueOrDefault(_state.CursorEnd);
_state.OverwriteMode = overwriteMode.GetValueOrDefault(_state.OverwriteMode);
_state.TypingEnabled = typingEnabled.GetValueOrDefault(_state.TypingEnabled);
// Reset the cursor blink.
_state.TextBoxBlinkCounter = 0;
// Tell the render thread there is something new to render.
Monitor.PulseAll(_stateLock);
}
}
public void UpdateCommandState(bool? acceptPressed, bool? cancelPressed, bool? controllerEnabled)
{
lock (_stateLock)
{
// Update the parameters that were provided.
_state.AcceptPressed = acceptPressed.GetValueOrDefault(_state.AcceptPressed);
_state.CancelPressed = cancelPressed.GetValueOrDefault(_state.CancelPressed);
_state.ControllerEnabled = controllerEnabled.GetValueOrDefault(_state.ControllerEnabled);
// Tell the render thread there is something new to render.
Monitor.PulseAll(_stateLock);
}
}
public void SetSurfaceInfo(RenderingSurfaceInfo surfaceInfo)
{
lock (_stateLock)
{
_state.SurfaceInfo = surfaceInfo;
// Tell the render thread there is something new to render.
Monitor.PulseAll(_stateLock);
}
}
internal bool DrawTo(IVirtualMemoryManager destination, ulong position)
{
return _renderer.WriteBufferToMemory(destination, position);
}
public void Dispose()
{
_textBoxBlinkTimedAction.RequestCancel();
_renderAction.RequestCancel();
}
}
}

View file

@ -0,0 +1,606 @@
using Ryujinx.HLE.Ui;
using Ryujinx.Memory;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Base class that generates the graphics for the software keyboard applet during inline mode.
/// </summary>
internal class SoftwareKeyboardRendererBase
{
public const int TextBoxBlinkThreshold = 8;
const string MessageText = "Please use the keyboard to input text";
const string AcceptText = "Accept";
const string CancelText = "Cancel";
const string ControllerToggleText = "Toggle input";
private readonly object _bufferLock = new object();
private RenderingSurfaceInfo _surfaceInfo = null;
private Image<Argb32> _surface = null;
private byte[] _bufferData = null;
private Image _ryujinxLogo = null;
private Image _padAcceptIcon = null;
private Image _padCancelIcon = null;
private Image _keyModeIcon = null;
private float _textBoxOutlineWidth;
private float _padPressedPenWidth;
private Color _textNormalColor;
private Color _textSelectedColor;
private Color _textOverCursorColor;
private IBrush _panelBrush;
private IBrush _disabledBrush;
private IBrush _cursorBrush;
private IBrush _selectionBoxBrush;
private Pen _textBoxOutlinePen;
private Pen _cursorPen;
private Pen _selectionBoxPen;
private Pen _padPressedPen;
private int _inputTextFontSize;
private Font _messageFont;
private Font _inputTextFont;
private Font _labelsTextFont;
private RectangleF _panelRectangle;
private Point _logoPosition;
private float _messagePositionY;
public SoftwareKeyboardRendererBase(IHostUiTheme uiTheme)
{
int ryujinxLogoSize = 32;
string ryujinxIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Logo_Ryujinx.png";
_ryujinxLogo = LoadResource(Assembly.GetExecutingAssembly(), ryujinxIconPath, ryujinxLogoSize, ryujinxLogoSize);
string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png";
string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png";
string keyModeIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png";
_padAcceptIcon = LoadResource(Assembly.GetExecutingAssembly(), padAcceptIconPath , 0, 0);
_padCancelIcon = LoadResource(Assembly.GetExecutingAssembly(), padCancelIconPath , 0, 0);
_keyModeIcon = LoadResource(Assembly.GetExecutingAssembly(), keyModeIconPath , 0, 0);
Color panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255);
Color panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150);
Color borderColor = ToColor(uiTheme.DefaultBorderColor);
Color selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor);
_textNormalColor = ToColor(uiTheme.DefaultForegroundColor);
_textSelectedColor = ToColor(uiTheme.SelectionForegroundColor);
_textOverCursorColor = ToColor(uiTheme.DefaultForegroundColor, null, true);
float cursorWidth = 2;
_textBoxOutlineWidth = 2;
_padPressedPenWidth = 2;
_panelBrush = new SolidBrush(panelColor);
_disabledBrush = new SolidBrush(panelTransparentColor);
_cursorBrush = new SolidBrush(_textNormalColor);
_selectionBoxBrush = new SolidBrush(selectionBackgroundColor);
_textBoxOutlinePen = new Pen(borderColor, _textBoxOutlineWidth);
_cursorPen = new Pen(_textNormalColor, cursorWidth);
_selectionBoxPen = new Pen(selectionBackgroundColor, cursorWidth);
_padPressedPen = new Pen(borderColor, _padPressedPenWidth);
_inputTextFontSize = 20;
CreateFonts(uiTheme.FontFamily);
}
private void CreateFonts(string uiThemeFontFamily)
{
// Try a list of fonts in case any of them is not available in the system.
string[] availableFonts = new string[]
{
uiThemeFontFamily,
"Liberation Sans",
"FreeSans",
"DejaVu Sans",
"Lucida Grande"
};
foreach (string fontFamily in availableFonts)
{
try
{
_messageFont = SystemFonts.CreateFont(fontFamily, 26, FontStyle.Regular);
_inputTextFont = SystemFonts.CreateFont(fontFamily, _inputTextFontSize, FontStyle.Regular);
_labelsTextFont = SystemFonts.CreateFont(fontFamily, 24, FontStyle.Regular);
return;
}
catch
{
}
}
throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!");
}
private Color ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false)
{
var a = (byte)(color.A * 255);
var r = (byte)(color.R * 255);
var g = (byte)(color.G * 255);
var b = (byte)(color.B * 255);
if (flipRgb)
{
r = (byte)(255 - r);
g = (byte)(255 - g);
b = (byte)(255 - b);
}
return Color.FromRgba(r, g, b, overrideAlpha.GetValueOrDefault(a));
}
private Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight)
{
Stream resourceStream = assembly.GetManifestResourceStream(resourcePath);
return LoadResource(resourceStream, newWidth, newHeight);
}
private Image LoadResource(Stream resourceStream, int newWidth, int newHeight)
{
Debug.Assert(resourceStream != null);
var image = Image.Load(resourceStream);
if (newHeight != 0 && newWidth != 0)
{
image.Mutate(x => x.Resize(newWidth, newHeight, KnownResamplers.Lanczos3));
}
return image;
}
private void SetGraphicsOptions(IImageProcessingContext context)
{
context.GetGraphicsOptions().Antialias = true;
context.GetShapeGraphicsOptions().GraphicsOptions.Antialias = true;
}
private void DrawImmutableElements()
{
if (_surface == null)
{
return;
}
_surface.Mutate(context =>
{
SetGraphicsOptions(context);
context.Clear(Color.Transparent);
context.Fill(_panelBrush, _panelRectangle);
context.DrawImage(_ryujinxLogo, _logoPosition, 1);
float halfWidth = _panelRectangle.Width / 2;
float buttonsY = _panelRectangle.Y + 185;
PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY);
DrawControllerToggle(context, disableButtonPosition);
});
}
public void DrawMutableElements(SoftwareKeyboardUiState state)
{
if (_surface == null)
{
return;
}
_surface.Mutate(context =>
{
var messageRectangle = MeasureString(MessageText, _messageFont);
float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.X;
float messagePositionY = _messagePositionY - messageRectangle.Y;
var messagePosition = new PointF(messagePositionX, messagePositionY);
var messageBoundRectangle = new RectangleF(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height);
SetGraphicsOptions(context);
context.Fill(_panelBrush, messageBoundRectangle);
context.DrawText(MessageText, _messageFont, _textNormalColor, messagePosition);
if (!state.TypingEnabled)
{
// Just draw a semi-transparent rectangle on top to fade the component with the background.
// TODO (caian): This will not work if one decides to add make background semi-transparent as well.
context.Fill(_disabledBrush, messageBoundRectangle);
}
DrawTextBox(context, state);
float halfWidth = _panelRectangle.Width / 2;
float buttonsY = _panelRectangle.Y + 185;
PointF acceptButtonPosition = new PointF(halfWidth - 180, buttonsY);
PointF cancelButtonPosition = new PointF(halfWidth , buttonsY);
PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY);
DrawPadButton(context, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled);
DrawPadButton(context, cancelButtonPosition, _padCancelIcon, CancelText, state.CancelPressed, state.ControllerEnabled);
});
}
public void CreateSurface(RenderingSurfaceInfo surfaceInfo)
{
if (_surfaceInfo != null)
{
return;
}
_surfaceInfo = surfaceInfo;
Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8);
// Use the whole area of the image to draw, even the alignment, otherwise it may shear the final
// image if the pitch is different.
uint totalWidth = _surfaceInfo.Pitch / 4;
uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch;
Debug.Assert(_surfaceInfo.Width <= totalWidth);
Debug.Assert(_surfaceInfo.Height <= totalHeight);
Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size);
_surface = new Image<Argb32>((int)totalWidth, (int)totalHeight);
ComputeConstants();
DrawImmutableElements();
}
private void ComputeConstants()
{
int totalWidth = (int)_surfaceInfo.Width;
int totalHeight = (int)_surfaceInfo.Height;
int panelHeight = 240;
int panelPositionY = totalHeight - panelHeight;
_panelRectangle = new RectangleF(0, panelPositionY, totalWidth, panelHeight);
_messagePositionY = panelPositionY + 60;
int logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2;
int logoPositionY = panelPositionY + 18;
_logoPosition = new Point(logoPositionX, logoPositionY);
}
private static RectangleF MeasureString(string text, Font font)
{
RendererOptions options = new RendererOptions(font);
if (text == "")
{
FontRectangle emptyRectangle = TextMeasurer.Measure(" ", options);
return new RectangleF(0, emptyRectangle.Y, 0, emptyRectangle.Height);
}
FontRectangle rectangle = TextMeasurer.Measure(text, options);
return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
}
private static RectangleF MeasureString(ReadOnlySpan<char> text, Font font)
{
RendererOptions options = new RendererOptions(font);
if (text == "")
{
FontRectangle emptyRectangle = TextMeasurer.Measure(" ", options);
return new RectangleF(0, emptyRectangle.Y, 0, emptyRectangle.Height);
}
FontRectangle rectangle = TextMeasurer.Measure(text, options);
return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
}
private void DrawTextBox(IImageProcessingContext context, SoftwareKeyboardUiState state)
{
var inputTextRectangle = MeasureString(state.InputText, _inputTextFont);
float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.X + 8));
float boxHeight = 32;
float boxY = _panelRectangle.Y + 110;
float boxX = (int)((_panelRectangle.Width - boxWidth) / 2);
RectangleF boxRectangle = new RectangleF(boxX, boxY, boxWidth, boxHeight);
RectangleF boundRectangle = new RectangleF(_panelRectangle.X, boxY - _textBoxOutlineWidth,
_panelRectangle.Width, boxHeight + 2 * _textBoxOutlineWidth);
context.Fill(_panelBrush, boundRectangle);
context.Draw(_textBoxOutlinePen, boxRectangle);
float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.X;
float inputTextY = boxY + 5;
var inputTextPosition = new PointF(inputTextX, inputTextY);
context.DrawText(state.InputText, _inputTextFont, _textNormalColor, inputTextPosition);
// Draw the cursor on top of the text and redraw the text with a different color if necessary.
Color cursorTextColor;
IBrush cursorBrush;
Pen cursorPen;
float cursorPositionYTop = inputTextY + 1;
float cursorPositionYBottom = cursorPositionYTop + _inputTextFontSize + 1;
float cursorPositionXLeft;
float cursorPositionXRight;
bool cursorVisible = false;
if (state.CursorBegin != state.CursorEnd)
{
Debug.Assert(state.InputText.Length > 0);
cursorTextColor = _textSelectedColor;
cursorBrush = _selectionBoxBrush;
cursorPen = _selectionBoxPen;
ReadOnlySpan<char> textUntilBegin = state.InputText.AsSpan(0, state.CursorBegin);
ReadOnlySpan<char> textUntilEnd = state.InputText.AsSpan(0, state.CursorEnd);
var selectionBeginRectangle = MeasureString(textUntilBegin, _inputTextFont);
var selectionEndRectangle = MeasureString(textUntilEnd , _inputTextFont);
cursorVisible = true;
cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.X;
cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.X;
}
else
{
cursorTextColor = _textOverCursorColor;
cursorBrush = _cursorBrush;
cursorPen = _cursorPen;
if (state.TextBoxBlinkCounter < TextBoxBlinkThreshold)
{
// Show the blinking cursor.
int cursorBegin = Math.Min(state.InputText.Length, state.CursorBegin);
ReadOnlySpan<char> textUntilCursor = state.InputText.AsSpan(0, cursorBegin);
var cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont);
cursorVisible = true;
cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
if (state.OverwriteMode)
{
// The blinking cursor is in overwrite mode so it takes the size of a character.
if (state.CursorBegin < state.InputText.Length)
{
textUntilCursor = state.InputText.AsSpan(0, cursorBegin + 1);
cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont);
cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
}
else
{
cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2;
}
}
else
{
// The blinking cursor is in insert mode so it is only a line.
cursorPositionXRight = cursorPositionXLeft;
}
}
else
{
cursorPositionXLeft = inputTextX;
cursorPositionXRight = inputTextX;
}
}
if (state.TypingEnabled && cursorVisible)
{
float cursorWidth = cursorPositionXRight - cursorPositionXLeft;
float cursorHeight = cursorPositionYBottom - cursorPositionYTop;
if (cursorWidth == 0)
{
PointF[] points = new PointF[]
{
new PointF(cursorPositionXLeft, cursorPositionYTop),
new PointF(cursorPositionXLeft, cursorPositionYBottom),
};
context.DrawLines(cursorPen, points);
}
else
{
var cursorRectangle = new RectangleF(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
context.Draw(cursorPen , cursorRectangle);
context.Fill(cursorBrush, cursorRectangle);
Image<Argb32> textOverCursor = new Image<Argb32>((int)cursorRectangle.Width, (int)cursorRectangle.Height);
textOverCursor.Mutate(context =>
{
var textRelativePosition = new PointF(inputTextPosition.X - cursorRectangle.X, inputTextPosition.Y - cursorRectangle.Y);
context.DrawText(state.InputText, _inputTextFont, cursorTextColor, textRelativePosition);
});
var cursorPosition = new Point((int)cursorRectangle.X, (int)cursorRectangle.Y);
context.DrawImage(textOverCursor, cursorPosition, 1);
}
}
else if (!state.TypingEnabled)
{
// Just draw a semi-transparent rectangle on top to fade the component with the background.
// TODO (caian): This will not work if one decides to add make background semi-transparent as well.
context.Fill(_disabledBrush, boundRectangle);
}
}
private void DrawPadButton(IImageProcessingContext context, PointF point, Image icon, string label, bool pressed, bool enabled)
{
// Use relative positions so we can center the the entire drawing later.
float iconX = 0;
float iconY = 0;
float iconWidth = icon.Width;
float iconHeight = icon.Height;
var labelRectangle = MeasureString(label, _labelsTextFont);
float labelPositionX = iconWidth + 8 - labelRectangle.X;
float labelPositionY = 3;
float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.X;
float fullHeight = iconHeight;
// Convert all relative positions into absolute.
float originX = (int)(point.X - fullWidth / 2);
float originY = (int)(point.Y - fullHeight / 2);
iconX += originX;
iconY += originY;
var iconPosition = new Point((int)iconX, (int)iconY);
var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY);
var selectedRectangle = new RectangleF(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth,
fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth);
var boundRectangle = new RectangleF(originX, originY, fullWidth, fullHeight);
boundRectangle.Inflate(4 * _padPressedPenWidth, 4 * _padPressedPenWidth);
context.Fill(_panelBrush, boundRectangle);
context.DrawImage(icon, iconPosition, 1);
context.DrawText(label, _labelsTextFont, _textNormalColor, labelPosition);
if (enabled)
{
if (pressed)
{
context.Draw(_padPressedPen, selectedRectangle);
}
}
else
{
// Just draw a semi-transparent rectangle on top to fade the component with the background.
// TODO (caian): This will not work if one decides to add make background semi-transparent as well.
context.Fill(_disabledBrush, boundRectangle);
}
}
private void DrawControllerToggle(IImageProcessingContext context, PointF point)
{
var labelRectangle = MeasureString(ControllerToggleText, _labelsTextFont);
// Use relative positions so we can center the the entire drawing later.
float keyWidth = _keyModeIcon.Width;
float keyHeight = _keyModeIcon.Height;
float labelPositionX = keyWidth + 8 - labelRectangle.X;
float labelPositionY = -labelRectangle.Y - 1;
float keyX = 0;
float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2);
float fullWidth = labelPositionX + labelRectangle.Width;
float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight);
// Convert all relative positions into absolute.
float originX = (int)(point.X - fullWidth / 2);
float originY = (int)(point.Y - fullHeight / 2);
keyX += originX;
keyY += originY;
var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY);
var overlayPosition = new Point((int)keyX, (int)keyY);
context.DrawImage(_keyModeIcon, overlayPosition, 1);
context.DrawText(ControllerToggleText, _labelsTextFont, _textNormalColor, labelPosition);
}
public void CopyImageToBuffer()
{
lock (_bufferLock)
{
if (_surface == null)
{
return;
}
// Convert the pixel format used in the image to the one used in the Switch surface.
if (!_surface.TryGetSinglePixelSpan(out Span<Argb32> pixels))
{
return;
}
_bufferData = MemoryMarshal.AsBytes(pixels).ToArray();
Span<uint> dataConvert = MemoryMarshal.Cast<byte, uint>(_bufferData);
Debug.Assert(_bufferData.Length == _surfaceInfo.Size);
for (int i = 0; i < dataConvert.Length; i++)
{
dataConvert[i] = BitOperations.RotateRight(dataConvert[i], 8);
}
}
}
public bool WriteBufferToMemory(IVirtualMemoryManager destination, ulong position)
{
lock (_bufferLock)
{
if (_bufferData == null)
{
return false;
}
try
{
destination.Write(position, _bufferData);
}
catch
{
return false;
}
return true;
}
}
}
}

View file

@ -0,0 +1,28 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Identifies the software keyboard state.
/// </summary>
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,13 @@
namespace Ryujinx.HLE.HOS.Applets
{
public struct SoftwareKeyboardUiArgs
{
public string HeaderText;
public string SubtitleText;
public string InitialText;
public string GuideText;
public string SubmitText;
public int StringLengthMin;
public int StringLengthMax;
}
}

View file

@ -0,0 +1,22 @@
using Ryujinx.HLE.Ui;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// TODO
/// </summary>
internal class SoftwareKeyboardUiState
{
public string InputText = "";
public int CursorBegin = 0;
public int CursorEnd = 0;
public bool AcceptPressed = false;
public bool CancelPressed = false;
public bool OverwriteMode = false;
public bool TypingEnabled = true;
public bool ControllerEnabled = true;
public int TextBoxBlinkCounter = 0;
public RenderingSurfaceInfo SurfaceInfo = null;
}
}

View file

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A structure used by SetUserWordInfo request to the software keyboard.
/// </summary>
[StructLayout(LayoutKind.Sequential, Size = 0x64)]
struct SoftwareKeyboardUserWord
{
// Unknown
}
}

View file

@ -0,0 +1,19 @@
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Wraps a type in a class so it gets stored in the GC managed heap. This is used as communication mechanism
/// between classed that need to be disposed and, thus, can't share their references.
/// </summary>
/// <typeparam name="T">The internal type.</typeparam>
class TRef<T>
{
public T Value;
public TRef() { }
public TRef(T value)
{
Value = value;
}
}
}

View file

@ -0,0 +1,186 @@
using System;
using System.Threading;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// A threaded executor of periodic actions that can be cancelled. The total execution time is optional
/// and, in this case, a progress is reported back to the action.
/// </summary>
class TimedAction
{
public const int MaxThreadSleep = 100;
private class SleepSubstepData
{
public readonly int SleepMilliseconds;
public readonly int SleepCount;
public readonly int SleepRemainderMilliseconds;
public SleepSubstepData(int sleepMilliseconds)
{
SleepMilliseconds = Math.Min(sleepMilliseconds, MaxThreadSleep);
SleepCount = sleepMilliseconds / SleepMilliseconds;
SleepRemainderMilliseconds = sleepMilliseconds - SleepCount * SleepMilliseconds;
}
}
private TRef<bool> _cancelled = null;
private Thread _thread = null;
private object _lock = new object();
public bool IsRunning
{
get
{
lock (_lock)
{
if (_thread == null)
{
return false;
}
return _thread.IsAlive;
}
}
}
public void RequestCancel()
{
lock (_lock)
{
if (_cancelled != null)
{
Volatile.Write(ref _cancelled.Value, true);
}
}
}
public TimedAction() { }
private void Reset(Thread thread, TRef<bool> cancelled)
{
lock (_lock)
{
// Cancel the current task.
if (_cancelled != null)
{
Volatile.Write(ref _cancelled.Value, true);
}
_cancelled = cancelled;
_thread = thread;
_thread.IsBackground = true;
_thread.Start();
}
}
public void Reset(Action<float> action, int totalMilliseconds, int sleepMilliseconds)
{
// Create a dedicated cancel token for each task.
var cancelled = new TRef<bool>(false);
Reset(new Thread(() =>
{
var substepData = new SleepSubstepData(sleepMilliseconds);
int totalCount = totalMilliseconds / sleepMilliseconds;
int totalRemainder = totalMilliseconds - totalCount * sleepMilliseconds;
if (Volatile.Read(ref cancelled.Value))
{
action(-1);
return;
}
action(0);
for (int i = 1; i <= totalCount; i++)
{
if (SleepWithSubstep(substepData, cancelled))
{
action(-1);
return;
}
action((float)(i * sleepMilliseconds) / totalMilliseconds);
}
if (totalRemainder > 0)
{
if (SleepWithSubstep(substepData, cancelled))
{
action(-1);
return;
}
action(1);
}
}), cancelled);
}
public void Reset(Action action, int sleepMilliseconds)
{
// Create a dedicated cancel token for each task.
var cancelled = new TRef<bool>(false);
Reset(new Thread(() =>
{
var substepData = new SleepSubstepData(sleepMilliseconds);
while (!Volatile.Read(ref cancelled.Value))
{
action();
if (SleepWithSubstep(substepData, cancelled))
{
return;
}
}
}), cancelled);
}
public void Reset(Action action)
{
// Create a dedicated cancel token for each task.
var cancelled = new TRef<bool>(false);
Reset(new Thread(() =>
{
while (!Volatile.Read(ref cancelled.Value))
{
action();
}
}), cancelled);
}
private static bool SleepWithSubstep(SleepSubstepData substepData, TRef<bool> cancelled)
{
for (int i = 0; i < substepData.SleepCount; i++)
{
if (Volatile.Read(ref cancelled.Value))
{
return true;
}
Thread.Sleep(substepData.SleepMilliseconds);
}
if (substepData.SleepRemainderMilliseconds > 0)
{
if (Volatile.Read(ref cancelled.Value))
{
return true;
}
Thread.Sleep(substepData.SleepRemainderMilliseconds);
}
return Volatile.Read(ref cancelled.Value);
}
}
}

View file

@ -0,0 +1,80 @@
using ARMeilleure.Memory;
using Ryujinx.Cpu;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.Memory;
namespace Ryujinx.HLE.HOS
{
interface IArmProcessContext : IProcessContext
{
IDiskCacheLoadState Initialize(
string titleIdText,
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize);
}
class ArmProcessContext<T> : IArmProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager
{
private readonly ulong _pid;
private readonly GpuContext _gpuContext;
private readonly ICpuContext _cpuContext;
private T _memoryManager;
public IVirtualMemoryManager AddressSpace => _memoryManager;
public ArmProcessContext(ulong pid, ICpuEngine cpuEngine, GpuContext gpuContext, T memoryManager, bool for64Bit)
{
if (memoryManager is IRefCounted rc)
{
rc.IncrementReferenceCount();
}
gpuContext.RegisterProcess(pid, memoryManager);
_pid = pid;
_gpuContext = gpuContext;
_cpuContext = cpuEngine.CreateCpuContext(memoryManager, for64Bit);
_memoryManager = memoryManager;
}
public IExecutionContext CreateExecutionContext(ExceptionCallbacks exceptionCallbacks)
{
return _cpuContext.CreateExecutionContext(exceptionCallbacks);
}
public void Execute(IExecutionContext context, ulong codeAddress)
{
_cpuContext.Execute(context, codeAddress);
}
public IDiskCacheLoadState Initialize(
string titleIdText,
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize)
{
_cpuContext.PrepareCodeRange(codeAddress, codeSize);
return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled);
}
public void InvalidateCacheRegion(ulong address, ulong size)
{
_cpuContext.InvalidateCacheRegion(address, size);
}
public void Dispose()
{
if (_memoryManager is IRefCounted rc)
{
rc.DecrementReferenceCount();
_memoryManager = null;
_gpuContext.UnregisterProcess(_pid);
}
}
}
}

View file

@ -0,0 +1,89 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Cpu;
using Ryujinx.Cpu.AppleHv;
using Ryujinx.Cpu.Jit;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Kernel;
using Ryujinx.HLE.HOS.Kernel.Process;
using Ryujinx.Memory;
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS
{
class ArmProcessContextFactory : IProcessContextFactory
{
private readonly ITickSource _tickSource;
private readonly GpuContext _gpu;
private readonly string _titleIdText;
private readonly string _displayVersion;
private readonly bool _diskCacheEnabled;
private readonly ulong _codeAddress;
private readonly ulong _codeSize;
public IDiskCacheLoadState DiskCacheLoadState { get; private set; }
public ArmProcessContextFactory(
ITickSource tickSource,
GpuContext gpu,
string titleIdText,
string displayVersion,
bool diskCacheEnabled,
ulong codeAddress,
ulong codeSize)
{
_tickSource = tickSource;
_gpu = gpu;
_titleIdText = titleIdText;
_displayVersion = displayVersion;
_diskCacheEnabled = diskCacheEnabled;
_codeAddress = codeAddress;
_codeSize = codeSize;
}
public IProcessContext Create(KernelContext context, ulong pid, ulong addressSpaceSize, InvalidAccessHandler invalidAccessHandler, bool for64Bit)
{
IArmProcessContext processContext;
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64 && for64Bit && context.Device.Configuration.UseHypervisor)
{
var cpuEngine = new HvEngine(_tickSource);
var memoryManager = new HvMemoryManager(context.Memory, addressSpaceSize, invalidAccessHandler);
processContext = new ArmProcessContext<HvMemoryManager>(pid, cpuEngine, _gpu, memoryManager, for64Bit);
}
else
{
MemoryManagerMode mode = context.Device.Configuration.MemoryManagerMode;
if (!MemoryBlock.SupportsFlags(MemoryAllocationFlags.ViewCompatible))
{
mode = MemoryManagerMode.SoftwarePageTable;
}
var cpuEngine = new JitEngine(_tickSource);
switch (mode)
{
case MemoryManagerMode.SoftwarePageTable:
var memoryManager = new MemoryManager(context.Memory, addressSpaceSize, invalidAccessHandler);
processContext = new ArmProcessContext<MemoryManager>(pid, cpuEngine, _gpu, memoryManager, for64Bit);
break;
case MemoryManagerMode.HostMapped:
case MemoryManagerMode.HostMappedUnsafe:
bool unsafeMode = mode == MemoryManagerMode.HostMappedUnsafe;
var memoryManagerHostMapped = new MemoryManagerHostMapped(context.Memory, addressSpaceSize, unsafeMode, invalidAccessHandler);
processContext = new ArmProcessContext<MemoryManagerHostMapped>(pid, cpuEngine, _gpu, memoryManagerHostMapped, for64Bit);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize);
return processContext;
}
}
}

View file

@ -0,0 +1,25 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class ArraySubscriptingExpression : BaseNode
{
private BaseNode _leftNode;
private BaseNode _subscript;
public ArraySubscriptingExpression(BaseNode leftNode, BaseNode subscript) : base(NodeType.ArraySubscriptingExpression)
{
_leftNode = leftNode;
_subscript = subscript;
}
public override void PrintLeft(TextWriter writer)
{
writer.Write("(");
_leftNode.Print(writer);
writer.Write(")[");
_subscript.Print(writer);
writer.Write("]");
}
}
}

View file

@ -0,0 +1,59 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class ArrayType : BaseNode
{
private BaseNode _base;
private BaseNode _dimensionExpression;
private string _dimensionString;
public ArrayType(BaseNode Base, BaseNode dimensionExpression = null) : base(NodeType.ArrayType)
{
_base = Base;
_dimensionExpression = dimensionExpression;
}
public ArrayType(BaseNode Base, string dimensionString) : base(NodeType.ArrayType)
{
_base = Base;
_dimensionString = dimensionString;
}
public override bool HasRightPart()
{
return true;
}
public override bool IsArray()
{
return true;
}
public override void PrintLeft(TextWriter writer)
{
_base.PrintLeft(writer);
}
public override void PrintRight(TextWriter writer)
{
// FIXME: detect if previous char was a ].
writer.Write(" ");
writer.Write("[");
if (_dimensionString != null)
{
writer.Write(_dimensionString);
}
else if (_dimensionExpression != null)
{
_dimensionExpression.Print(writer);
}
writer.Write("]");
_base.PrintRight(writer);
}
}
}

View file

@ -0,0 +1,113 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public enum NodeType
{
CvQualifierType,
SimpleReferenceType,
NameType,
EncodedFunction,
NestedName,
SpecialName,
LiteralOperator,
NodeArray,
ElaboratedType,
PostfixQualifiedType,
SpecialSubstitution,
ExpandedSpecialSubstitution,
CtorDtorNameType,
EnclosedExpression,
ForwardTemplateReference,
NameTypeWithTemplateArguments,
PackedTemplateArgument,
TemplateArguments,
BooleanExpression,
CastExpression,
CallExpression,
IntegerCastExpression,
PackedTemplateParameter,
PackedTemplateParameterExpansion,
IntegerLiteral,
DeleteExpression,
MemberExpression,
ArraySubscriptingExpression,
InitListExpression,
PostfixExpression,
ConditionalExpression,
ThrowExpression,
FunctionParameter,
ConversionExpression,
BinaryExpression,
PrefixExpression,
BracedExpression,
BracedRangeExpression,
NewExpression,
QualifiedName,
StdQualifiedName,
DtOrName,
GlobalQualifiedName,
NoexceptSpec,
DynamicExceptionSpec,
FunctionType,
PointerType,
ReferenceType,
ConversionOperatorType,
LocalName,
CtorVtableSpecialName,
ArrayType
}
public abstract class BaseNode
{
public NodeType Type { get; protected set; }
public BaseNode(NodeType type)
{
Type = type;
}
public virtual void Print(TextWriter writer)
{
PrintLeft(writer);
if (HasRightPart())
{
PrintRight(writer);
}
}
public abstract void PrintLeft(TextWriter writer);
public virtual bool HasRightPart()
{
return false;
}
public virtual bool IsArray()
{
return false;
}
public virtual bool HasFunctions()
{
return false;
}
public virtual string GetName()
{
return null;
}
public virtual void PrintRight(TextWriter writer) {}
public override string ToString()
{
StringWriter writer = new StringWriter();
Print(writer);
return writer.ToString();
}
}
}

View file

@ -0,0 +1,41 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class BinaryExpression : BaseNode
{
private BaseNode _leftPart;
private string _name;
private BaseNode _rightPart;
public BinaryExpression(BaseNode leftPart, string name, BaseNode rightPart) : base(NodeType.BinaryExpression)
{
_leftPart = leftPart;
_name = name;
_rightPart = rightPart;
}
public override void PrintLeft(TextWriter writer)
{
if (_name.Equals(">"))
{
writer.Write("(");
}
writer.Write("(");
_leftPart.Print(writer);
writer.Write(") ");
writer.Write(_name);
writer.Write(" (");
_rightPart.Print(writer);
writer.Write(")");
if (_name.Equals(">"))
{
writer.Write(")");
}
}
}
}

View file

@ -0,0 +1,40 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class BracedExpression : BaseNode
{
private BaseNode _element;
private BaseNode _expression;
private bool _isArrayExpression;
public BracedExpression(BaseNode element, BaseNode expression, bool isArrayExpression) : base(NodeType.BracedExpression)
{
_element = element;
_expression = expression;
_isArrayExpression = isArrayExpression;
}
public override void PrintLeft(TextWriter writer)
{
if (_isArrayExpression)
{
writer.Write("[");
_element.Print(writer);
writer.Write("]");
}
else
{
writer.Write(".");
_element.Print(writer);
}
if (!_expression.GetType().Equals(NodeType.BracedExpression) || !_expression.GetType().Equals(NodeType.BracedRangeExpression))
{
writer.Write(" = ");
}
_expression.Print(writer);
}
}
}

View file

@ -0,0 +1,34 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class BracedRangeExpression : BaseNode
{
private BaseNode _firstNode;
private BaseNode _lastNode;
private BaseNode _expression;
public BracedRangeExpression(BaseNode firstNode, BaseNode lastNode, BaseNode expression) : base(NodeType.BracedRangeExpression)
{
_firstNode = firstNode;
_lastNode = lastNode;
_expression = expression;
}
public override void PrintLeft(TextWriter writer)
{
writer.Write("[");
_firstNode.Print(writer);
writer.Write(" ... ");
_lastNode.Print(writer);
writer.Write("]");
if (!_expression.GetType().Equals(NodeType.BracedExpression) || !_expression.GetType().Equals(NodeType.BracedRangeExpression))
{
writer.Write(" = ");
}
_expression.Print(writer);
}
}
}

View file

@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class CallExpression : NodeArray
{
private BaseNode _callee;
public CallExpression(BaseNode callee, List<BaseNode> nodes) : base(nodes, NodeType.CallExpression)
{
_callee = callee;
}
public override void PrintLeft(TextWriter writer)
{
_callee.Print(writer);
writer.Write("(");
writer.Write(string.Join<BaseNode>(", ", Nodes.ToArray()));
writer.Write(")");
}
}
}

View file

@ -0,0 +1,28 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class CastExpression : BaseNode
{
private string _kind;
private BaseNode _to;
private BaseNode _from;
public CastExpression(string kind, BaseNode to, BaseNode from) : base(NodeType.CastExpression)
{
_kind = kind;
_to = to;
_from = from;
}
public override void PrintLeft(TextWriter writer)
{
writer.Write(_kind);
writer.Write("<");
_to.PrintLeft(writer);
writer.Write(">(");
_from.PrintLeft(writer);
writer.Write(")");
}
}
}

View file

@ -0,0 +1,29 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class ConditionalExpression : BaseNode
{
private BaseNode _thenNode;
private BaseNode _elseNode;
private BaseNode _conditionNode;
public ConditionalExpression(BaseNode conditionNode, BaseNode thenNode, BaseNode elseNode) : base(NodeType.ConditionalExpression)
{
_thenNode = thenNode;
_conditionNode = conditionNode;
_elseNode = elseNode;
}
public override void PrintLeft(TextWriter writer)
{
writer.Write("(");
_conditionNode.Print(writer);
writer.Write(") ? (");
_thenNode.Print(writer);
writer.Write(") : (");
_elseNode.Print(writer);
writer.Write(")");
}
}
}

View file

@ -0,0 +1,24 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class ConversionExpression : BaseNode
{
private BaseNode _typeNode;
private BaseNode _expressions;
public ConversionExpression(BaseNode typeNode, BaseNode expressions) : base(NodeType.ConversionExpression)
{
_typeNode = typeNode;
_expressions = expressions;
}
public override void PrintLeft(TextWriter writer)
{
writer.Write("(");
_typeNode.Print(writer);
writer.Write(")(");
_expressions.Print(writer);
}
}
}

View file

@ -0,0 +1,15 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Diagnostics.Demangler.Ast
{
public class ConversionOperatorType : ParentNode
{
public ConversionOperatorType(BaseNode child) : base(NodeType.ConversionOperatorType, child) { }
public override void PrintLeft(TextWriter writer)
{
writer.Write("operator ");
Child.Print(writer);
}
}
}

Some files were not shown because too many files have changed in this diff Show more