diff --git a/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs b/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs index 8de630e..e0cf61e 100644 --- a/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs +++ b/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs @@ -3,6 +3,7 @@ using SharpEmu.HLE; using System.Buffers.Binary; +using System.Text.RegularExpressions; namespace SharpEmu.Libs.PlayGo; @@ -14,15 +15,35 @@ public static class PlayGoExports private const int OrbisPlayGoErrorBadHandle = unchecked((int)0x80B20009); private const int OrbisPlayGoErrorBadPointer = unchecked((int)0x80B2000A); private const int OrbisPlayGoErrorBadSize = unchecked((int)0x80B2000B); + private const int OrbisPlayGoErrorBadChunkId = unchecked((int)0x80B2000C); private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E); + private const int OrbisPlayGoErrorBadLocus = unchecked((int)0x80B20010); private const ulong PlayGoInitBufAddrOffset = 0; private const ulong PlayGoInitBufSizeOffset = 8; private const uint PlayGoMinimumInitBufferSize = 0x200000; private const uint PlayGoHandle = 1; + private const int PlayGoLocusNotDownloaded = 0; + private const int PlayGoLocusLocalSlow = 2; + private const int PlayGoLocusLocalFast = 3; + private const int PlayGoInstallSpeedSuspended = 0; + private const int PlayGoInstallSpeedTrickle = 1; + private const int PlayGoInstallSpeedFull = 2; + private const uint MaxPlayGoQueryEntries = 0x4000; + + private static readonly Regex ChunkIdPattern = new( + @"]*\bid\s*=\s*""(?\d+)""", + RegexOptions.CultureInvariant); + + private static readonly Regex DefaultChunkPattern = new( + @"default_chunk\s*=\s*""(?\d+)""", + RegexOptions.CultureInvariant); private static readonly object _stateGate = new(); private static bool _initialized; - private static bool _hasPlayGoData; + private static bool _opened; + private static PlayGoMetadata _metadata = PlayGoMetadata.Empty; + private static int _installSpeed = PlayGoInstallSpeedTrickle; + private static ulong _languageMask = ulong.MaxValue; [SysAbiExport( Nid = "ts6GlZOKRrE", @@ -67,7 +88,10 @@ public static class PlayGoExports return OrbisPlayGoErrorAlreadyInitialized; } - _hasPlayGoData = HasPlayGoChunkData(); + _metadata = LoadPlayGoMetadata(); + _installSpeed = PlayGoInstallSpeedTrickle; + _languageMask = ulong.MaxValue; + _opened = false; _initialized = true; } @@ -100,10 +124,12 @@ public static class PlayGoExports return OrbisPlayGoErrorNotInitialized; } - if (!_hasPlayGoData) + if (!_metadata.Available) { return OrbisPlayGoErrorNotSupportPlayGo; } + + _opened = true; } Span handleBytes = stackalloc byte[sizeof(uint)]; @@ -132,7 +158,8 @@ public static class PlayGoExports } _initialized = false; - _hasPlayGoData = false; + _opened = false; + _metadata = PlayGoMetadata.Empty; } return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -157,20 +184,567 @@ public static class PlayGoExports { return OrbisPlayGoErrorBadHandle; } + + _opened = false; } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } - private static bool HasPlayGoChunkData() + [SysAbiExport( + Nid = "73fF1MFU8hA", + ExportName = "scePlayGoGetChunkId", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoGetChunkId(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var outChunkIdList = ctx[CpuRegister.Rsi]; + var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]); + var outEntries = ctx[CpuRegister.Rcx]; + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (outEntries == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + if (outChunkIdList != 0 && numberOfEntries == 0) + { + return OrbisPlayGoErrorBadSize; + } + + ushort[] chunkIds; + lock (_stateGate) + { + chunkIds = _metadata.ChunkIds; + } + + var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length; + if (outChunkIdList == 0) + { + return TryWriteUInt32(ctx, outEntries, availableEntries) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + var entriesToWrite = Math.Min(numberOfEntries, availableEntries); + if (entriesToWrite > MaxPlayGoQueryEntries) + { + return OrbisPlayGoErrorBadSize; + } + + for (uint i = 0; i < entriesToWrite; i++) + { + var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i]; + if (!TryWriteUInt16(ctx, outChunkIdList + (i * sizeof(ushort)), chunkId)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + } + + return TryWriteUInt32(ctx, outEntries, entriesToWrite) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "v6EZ-YWRdMs", + ExportName = "scePlayGoGetEta", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoGetEta(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var chunkIds = ctx[CpuRegister.Rsi]; + var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]); + var outEta = ctx[CpuRegister.Rcx]; + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (chunkIds == 0 || outEta == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries) + { + return OrbisPlayGoErrorBadSize; + } + + return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0 + ? chunkError + : TryWriteInt64(ctx, outEta, 0) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "rvBSfTimejE", + ExportName = "scePlayGoGetInstallSpeed", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoGetInstallSpeed(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var outSpeed = ctx[CpuRegister.Rsi]; + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (outSpeed == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + int speed; + lock (_stateGate) + { + speed = _installSpeed; + } + + return TryWriteInt32(ctx, outSpeed, speed) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "3OMbYZBaa50", + ExportName = "scePlayGoGetLanguageMask", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoGetLanguageMask(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var outLanguageMask = ctx[CpuRegister.Rsi]; + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (outLanguageMask == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + ulong languageMask; + lock (_stateGate) + { + languageMask = _languageMask; + } + + return ctx.TryWriteUInt64(outLanguageMask, languageMask) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "uWIYLFkkwqk", + ExportName = "scePlayGoGetLocus", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoGetLocus(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var chunkIds = ctx[CpuRegister.Rsi]; + var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]); + var outLoci = ctx[CpuRegister.Rcx]; + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (chunkIds == 0 || outLoci == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries) + { + return OrbisPlayGoErrorBadSize; + } + + var loci = new byte[numberOfEntries]; + for (uint i = 0; i < numberOfEntries; i++) + { + if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (!IsKnownChunkId(chunkId)) + { + loci[i] = PlayGoLocusNotDownloaded; + return ctx.Memory.TryWrite(outLoci, loci) + ? OrbisPlayGoErrorBadChunkId + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + loci[i] = PlayGoLocusLocalFast; + } + + return ctx.Memory.TryWrite(outLoci, loci) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "-RJWNMK3fC8", + ExportName = "scePlayGoGetProgress", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoGetProgress(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var chunkIds = ctx[CpuRegister.Rsi]; + var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]); + var outProgress = ctx[CpuRegister.Rcx]; + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (chunkIds == 0 || outProgress == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries) + { + return OrbisPlayGoErrorBadSize; + } + + var chunkError = ValidateChunkIds(ctx, chunkIds, numberOfEntries); + if (chunkError != 0) + { + return chunkError; + } + + Span progress = stackalloc byte[sizeof(ulong) * 2]; + BinaryPrimitives.WriteUInt64LittleEndian(progress, 0); + BinaryPrimitives.WriteUInt64LittleEndian(progress[sizeof(ulong)..], 0); + return ctx.Memory.TryWrite(outProgress, progress) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "Nn7zKwnA5q0", + ExportName = "scePlayGoGetToDoList", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoGetToDoList(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var outTodoList = ctx[CpuRegister.Rsi]; + var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]); + var outEntries = ctx[CpuRegister.Rcx]; + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (outTodoList == 0 || outEntries == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + if (numberOfEntries == 0) + { + return OrbisPlayGoErrorBadSize; + } + + return TryWriteUInt32(ctx, outEntries, 0) + ? (int)OrbisGen2Result.ORBIS_GEN2_OK + : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + [SysAbiExport( + Nid = "-Q1-u1a7p0g", + ExportName = "scePlayGoPrefetch", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoPrefetch(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var chunkIds = ctx[CpuRegister.Rsi]; + var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]); + var minimumLocus = unchecked((int)ctx[CpuRegister.Rcx]); + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (chunkIds == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries) + { + return OrbisPlayGoErrorBadSize; + } + + if (minimumLocus is not PlayGoLocusNotDownloaded and not PlayGoLocusLocalSlow and not PlayGoLocusLocalFast) + { + return OrbisPlayGoErrorBadLocus; + } + + return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0 + ? chunkError + : (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "4AAcTU9R3XM", + ExportName = "scePlayGoSetInstallSpeed", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoSetInstallSpeed(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var speed = unchecked((int)ctx[CpuRegister.Rsi]); + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (speed is not PlayGoInstallSpeedSuspended and not PlayGoInstallSpeedTrickle and not PlayGoInstallSpeedFull) + { + return OrbisPlayGoErrorInvalidArgument; + } + + lock (_stateGate) + { + _installSpeed = speed; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "LosLlHOpNqQ", + ExportName = "scePlayGoSetLanguageMask", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoSetLanguageMask(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var languageMask = ctx[CpuRegister.Rsi]; + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + lock (_stateGate) + { + _languageMask = languageMask; + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + [SysAbiExport( + Nid = "gUPGiOQ1tmQ", + ExportName = "scePlayGoSetToDoList", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePlayGo")] + public static int PlayGoSetToDoList(CpuContext ctx) + { + var handle = unchecked((uint)ctx[CpuRegister.Rdi]); + var todoList = ctx[CpuRegister.Rsi]; + var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]); + + var validation = ValidateHandle(handle); + if (validation != 0) + { + return validation; + } + + if (todoList == 0) + { + return OrbisPlayGoErrorBadPointer; + } + + return numberOfEntries == 0 + ? OrbisPlayGoErrorBadSize + : (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static int ValidateHandle(uint handle) + { + lock (_stateGate) + { + if (!_initialized) + { + return OrbisPlayGoErrorNotInitialized; + } + + if (handle != PlayGoHandle || !_opened) + { + return OrbisPlayGoErrorBadHandle; + } + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static int ValidateChunkIds(CpuContext ctx, ulong chunkIds, uint numberOfEntries) + { + for (uint i = 0; i < numberOfEntries; i++) + { + if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (!IsKnownChunkId(chunkId)) + { + return OrbisPlayGoErrorBadChunkId; + } + } + + return (int)OrbisGen2Result.ORBIS_GEN2_OK; + } + + private static bool IsKnownChunkId(ushort chunkId) + { + lock (_stateGate) + { + return _metadata.ChunkIds.Length == 0 || Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0; + } + } + + private static PlayGoMetadata LoadPlayGoMetadata() { var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); if (string.IsNullOrWhiteSpace(app0Root)) { + return PlayGoMetadata.Empty; + } + + var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat"); + var scenarioJson = Path.Combine(app0Root, "sce_sys", "playgo-scenario.json"); + var chunkDefsXml = Path.Combine(app0Root, "playgo-chunkdefs.xml"); + + var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml); + if (!hasMetadata) + { + return PlayGoMetadata.Empty; + } + + var chunkIds = LoadChunkIds(chunkDefsXml); + return new PlayGoMetadata(true, chunkIds); + } + + private static ushort[] LoadChunkIds(string chunkDefsXml) + { + if (!File.Exists(chunkDefsXml)) + { + return Array.Empty(); + } + + try + { + var xml = File.ReadAllText(chunkDefsXml); + var chunkIds = new HashSet(); + AddChunkIds(xml, DefaultChunkPattern, chunkIds); + AddChunkIds(xml, ChunkIdPattern, chunkIds); + + var sorted = chunkIds.ToArray(); + Array.Sort(sorted); + return sorted; + } + catch (IOException) + { + return Array.Empty(); + } + catch (UnauthorizedAccessException) + { + return Array.Empty(); + } + } + + private static void AddChunkIds(string xml, Regex pattern, HashSet chunkIds) + { + foreach (Match match in pattern.Matches(xml)) + { + if (ushort.TryParse(match.Groups["id"].Value, out var chunkId)) + { + chunkIds.Add(chunkId); + } + } + } + + private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value) + { + Span buffer = stackalloc byte[sizeof(ushort)]; + if (!ctx.Memory.TryRead(address, buffer)) + { + value = 0; return false; } - var hostPath = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat"); - return File.Exists(hostPath); + value = BinaryPrimitives.ReadUInt16LittleEndian(buffer); + return true; + } + + private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value) + { + Span buffer = stackalloc byte[sizeof(ushort)]; + BinaryPrimitives.WriteUInt16LittleEndian(buffer, value); + return ctx.Memory.TryWrite(address, buffer); + } + + private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value) + { + Span buffer = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + return ctx.Memory.TryWrite(address, buffer); + } + + private static bool TryWriteInt32(CpuContext ctx, ulong address, int value) + { + Span buffer = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32LittleEndian(buffer, value); + return ctx.Memory.TryWrite(address, buffer); + } + + private static bool TryWriteInt64(CpuContext ctx, ulong address, long value) + { + Span buffer = stackalloc byte[sizeof(long)]; + BinaryPrimitives.WriteInt64LittleEndian(buffer, value); + return ctx.Memory.TryWrite(address, buffer); + } + + private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds) + { + public static readonly PlayGoMetadata Empty = new(false, Array.Empty()); } }