From 3ebfc56d4c81d37326019bd75e20970dacdef94c Mon Sep 17 00:00:00 2001 From: Youss <156013413+ftre120@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:33:30 +0200 Subject: [PATCH] [PlayGo] Derive the installed chunk set from the pak files on disk (#438) A title that ships no PlayGo sidecar was reported as a single-chunk package. That is wrong for any package whose content is split across chunks: the title is told everything past chunk 0 is not installed, even though a locally dumped title has all of its data present. The Invincible (PPSA06426) ships pakchunk0..8 and asks PlayGo which of those are available. Receiving BAD_CHUNK_ID for chunks 1..8, it re-queried scePlayGoGetLocus for the same chunk in a tight loop that never terminated (observed ~1000 consecutive dispatches with identical arguments). Discover the chunk ids from the pakchunk-.pak files present under the app0 root instead. Those N are exactly the chunks the package has, so the answer is derived from the install rather than assumed. Chunk 0 is always included, so a title with no pak files at all keeps the previous single-chunk behaviour, and ids outside the discovered set still return BAD_CHUNK_ID so title-side chunk enumeration still terminates. Verified on The Invincible: the discovered set is [0..8], matching the nine pak files, and the GetLocus retry loop no longer occurs. No regression on Dead Cells (PPSA15552): still reaches AGC rendering and presents frames. The existing metadata-free contract test still passes -- its app0 fixture has no pak files, so the discovered set stays [0]. --- src/SharpEmu.Libs/PlayGo/PlayGoExports.cs | 57 ++++++++++++++++++++--- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs b/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs index 5673b237..61ac102e 100644 --- a/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs +++ b/src/SharpEmu.Libs/PlayGo/PlayGoExports.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2026 SharpEmu Emulator Project +// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; @@ -698,14 +698,22 @@ public static class PlayGoExports var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml); if (!hasMetadata) { - // No PlayGo sidecar: report a fully-installed single chunk. Available must - // stay true or scePlayGoOpen fails with NotSupportPlayGo (fatal PS5-component - // init failure for UE titles); chunk 0 reports LocalFast and every other id - // returns BAD_CHUNK_ID, terminating title-side chunk enumeration. - TracePlayGo("metadata_missing; fully-installed single chunk"); + // No PlayGo sidecar: derive the installed chunk set from the pak files + // actually present on disk. A locally dumped title has all of its data + // installed, and a package that splits content across chunks names them + // pakchunk-.pak, so those N are exactly the chunks that + // exist. Reporting only chunk 0 told such a title its remaining content + // was missing: The Invincible (PPSA06426) ships pakchunk0..8 and spun + // forever re-querying scePlayGoGetLocus for a chunk that never became + // available. Available must stay true or scePlayGoOpen fails with + // NotSupportPlayGo (fatal PS5-component init failure for UE titles). + // Ids outside the discovered set still return BAD_CHUNK_ID, so + // title-side chunk enumeration still terminates. + var installedChunkIds = DiscoverInstalledChunkIds(app0Root); + TracePlayGo($"metadata_missing; fully-installed chunks=[{string.Join(',', installedChunkIds)}]"); return new PlayGoMetadata( true, - [(ushort)0], + installedChunkIds, PlayGoChunkIdKnowledge.Authoritative); } @@ -718,6 +726,41 @@ public static class PlayGoExports : PlayGoChunkIdKnowledge.Authoritative); } + // Chunk ids for a title that ships no PlayGo sidecar, taken from the + // pakchunk-.pak files on disk. Chunk 0 is always included: it + // is the base chunk and must resolve even for a title with no pak files at + // all (which keeps the single-chunk behaviour for such titles). + private static ushort[] DiscoverInstalledChunkIds(string app0Root) + { + var ids = new SortedSet { 0 }; + try + { + foreach (var pakFile in Directory.EnumerateFiles(app0Root, "pakchunk*.pak", SearchOption.AllDirectories)) + { + var name = Path.GetFileNameWithoutExtension(pakFile); + var digits = name.AsSpan("pakchunk".Length); + var length = 0; + while (length < digits.Length && char.IsAsciiDigit(digits[length])) + { + length++; + } + + if (length > 0 && ushort.TryParse(digits[..length], out var chunkId)) + { + ids.Add(chunkId); + } + } + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + + return ids.ToArray(); + } + private static ushort[] LoadChunkIds(string chunkDefsXml) { if (!File.Exists(chunkDefsXml))