// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; using System.Buffers.Binary; using System.Text.RegularExpressions; namespace SharpEmu.Libs.PlayGo; public static class PlayGoExports { private const int OrbisPlayGoErrorInvalidArgument = unchecked((int)0x80B20004); private const int OrbisPlayGoErrorNotInitialized = unchecked((int)0x80B20005); private const int OrbisPlayGoErrorAlreadyInitialized = unchecked((int)0x80B20006); 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 _opened; private static PlayGoMetadata _metadata = PlayGoMetadata.Empty; private static int _installSpeed = PlayGoInstallSpeedTrickle; private static ulong _languageMask = ulong.MaxValue; [SysAbiExport( Nid = "ts6GlZOKRrE", ExportName = "scePlayGoInitialize", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libScePlayGo")] public static int PlayGoInitialize(CpuContext ctx) { var initParamsAddress = ctx[CpuRegister.Rdi]; if (initParamsAddress == 0) { return OrbisPlayGoErrorBadPointer; } if (!ctx.TryReadUInt64(initParamsAddress + PlayGoInitBufAddrOffset, out var bufferAddress)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } Span bufferSizeBytes = stackalloc byte[sizeof(uint)]; if (!ctx.Memory.TryRead(initParamsAddress + PlayGoInitBufSizeOffset, bufferSizeBytes)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } var bufferSize = BinaryPrimitives.ReadUInt32LittleEndian(bufferSizeBytes); if (bufferAddress == 0) { return OrbisPlayGoErrorBadPointer; } if (bufferSize < PlayGoMinimumInitBufferSize) { return OrbisPlayGoErrorBadSize; } lock (_stateGate) { if (_initialized) { return OrbisPlayGoErrorAlreadyInitialized; } _metadata = LoadPlayGoMetadata(); _installSpeed = PlayGoInstallSpeedTrickle; _languageMask = ulong.MaxValue; _opened = false; _initialized = true; } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "M1Gma1ocrGE", ExportName = "scePlayGoOpen", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libScePlayGo")] public static int PlayGoOpen(CpuContext ctx) { var outHandleAddress = ctx[CpuRegister.Rdi]; var paramAddress = ctx[CpuRegister.Rsi]; if (outHandleAddress == 0) { return OrbisPlayGoErrorBadPointer; } if (paramAddress != 0) { return OrbisPlayGoErrorInvalidArgument; } lock (_stateGate) { if (!_initialized) { return OrbisPlayGoErrorNotInitialized; } if (!_metadata.Available) { return OrbisPlayGoErrorNotSupportPlayGo; } _opened = true; } Span handleBytes = stackalloc byte[sizeof(uint)]; BinaryPrimitives.WriteUInt32LittleEndian(handleBytes, PlayGoHandle); if (!ctx.Memory.TryWrite(outHandleAddress, handleBytes)) { return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "MPe0EeBGM-E", ExportName = "scePlayGoTerminate", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libScePlayGo")] public static int PlayGoTerminate(CpuContext ctx) { _ = ctx; lock (_stateGate) { if (!_initialized) { return OrbisPlayGoErrorNotInitialized; } _initialized = false; _opened = false; _metadata = PlayGoMetadata.Empty; } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "Uco1I0dlDi8", ExportName = "scePlayGoClose", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libScePlayGo")] public static int PlayGoClose(CpuContext ctx) { var handle = unchecked((uint)ctx[CpuRegister.Rdi]); lock (_stateGate) { if (!_initialized) { return OrbisPlayGoErrorNotInitialized; } if (handle != PlayGoHandle) { return OrbisPlayGoErrorBadHandle; } _opened = false; } return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [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; } 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()); } }