From 9e9a0f0d7d19ede7c3c8def80f51d128eeb8d19e Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 15 May 2026 17:31:51 +0200 Subject: [PATCH 01/11] DVD file overlay support Fixes #163 Basically allows games to "overlay" a set of file paths over the DVD. These are provided as a flat list of absolute paths, and when an overlayed file is read, the game gets the ability to provide its own read/seek callbacks. The current implementation rebuilds the FST. This means EntryNums will not be consistent with the original disc, but it does avoid rewriting a significant chunk of the existing DVD code that relies on the FST. --- include/aurora/dvd.h | 17 +++ lib/dolphin/dvd/dvd.cpp | 318 +++++++++++++++++++++++++++++++++------- 2 files changed, 280 insertions(+), 55 deletions(-) diff --git a/include/aurora/dvd.h b/include/aurora/dvd.h index 4a7f53b834..d4cd9f4f80 100644 --- a/include/aurora/dvd.h +++ b/include/aurora/dvd.h @@ -20,6 +20,23 @@ bool aurora_dvd_open(const char* disc_path); */ void aurora_dvd_close(void); +typedef struct AuroraOverlayFile { + const char* fileName; + void* userData; + size_t size; +} AuroraOverlayFile; + +void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles); + +typedef struct AuroraOverlayCallbacks { + void* (*open)(void* userdata); + void (*close)(void* handle); + int64_t (*read)(void* handle, uint8_t *buf, size_t len); + int64_t (*seek)(void* handle, int64_t offset, int32_t whence); +} AuroraOverlayCallbacks; + +void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks); + #ifdef __cplusplus } #endif diff --git a/lib/dolphin/dvd/dvd.cpp b/lib/dolphin/dvd/dvd.cpp index 9a054b5778..3b3dc08d34 100644 --- a/lib/dolphin/dvd/dvd.cpp +++ b/lib/dolphin/dvd/dvd.cpp @@ -1,5 +1,7 @@ #include #include + +#include #include #include #include @@ -8,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -16,19 +19,63 @@ namespace { +aurora::Module Log("aurora::dvd"); + struct FSTEntry { std::string name; bool isDir = false; u32 parent = 0; u32 nextOrLength = 0; + void* overlayData = nullptr; + // Original entry num on the base game disc, BEFORE being re-organized by overlays. + u32 origEntryNum = 0; +}; + +struct IterateNode { + std::string name; + bool isDir; + u32 originalEntryNum; + u32 size; + void* overlayData; + std::vector> children; + + IterateNode(std::string name, bool isDir, u32 size, void* overlayData) + : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(0), overlayData(overlayData) {} + + IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum) + : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(nullptr) {} }; struct IterateContext { - std::vector* entries = nullptr; - std::vector> dirStack; + std::shared_ptr root; + std::vector, u32>> dirStack; }; -NodHandle* s_disc = nullptr; +class CommandDataBase { +public: + virtual ~CommandDataBase() = default; + virtual int64_t read(uint8_t *buf, size_t len) = 0; + virtual int64_t seek(int64_t offset, int32_t whence) = 0; +}; + +class CommandDataNod final : public CommandDataBase { +public: + NodHandle* handle; + explicit CommandDataNod(NodHandle* nod_handle) : handle(nod_handle) { } + ~CommandDataNod() override { + nod_free(handle); + } + + int64_t read(uint8_t* buf, size_t len) override { + return nod_read(handle, buf, len); + } + + int64_t seek(int64_t offset, int32_t whence) override { + return nod_seek(handle, offset, whence); + } +}; + +CommandDataNod* s_disc; NodHandle* s_partition = nullptr; std::vector s_fstEntries; s32 s_currentDir = 0; @@ -38,6 +85,32 @@ BOOL s_autoFatalMessaging = FALSE; DVDDiskID s_diskID = {}; DVDLowCallback s_resetCoverCallback = nullptr; bool s_initialized = false; +AuroraOverlayCallbacks s_overlayCallbacks; + +class CommandDataOverlay final : public CommandDataBase { +public: + void* handle; + explicit CommandDataOverlay(void* handle) : handle(handle) { } + ~CommandDataOverlay() override { + s_overlayCallbacks.close(handle); + } + + int64_t read(uint8_t* buf, size_t len) override { + return s_overlayCallbacks.read(handle, buf, len); + } + + int64_t seek(int64_t offset, int32_t whence) override { + return s_overlayCallbacks.seek(handle, offset, whence); + } +}; + +struct OverlayFileEntry { + std::string fileName; + void* userData; + u32 size; +}; + +std::vector s_overlayFiles; void clearState() { if (s_partition != nullptr) { @@ -45,7 +118,7 @@ void clearState() { s_partition = nullptr; } if (s_disc != nullptr) { - nod_free(s_disc); + delete s_disc; s_disc = nullptr; } s_fstEntries.clear(); @@ -105,60 +178,140 @@ void sdlStreamClose(void* userData) { u32 fstCallback(u32 index, NodNodeKind kind, const char* name, u32 size, void* userData) { auto* ctx = static_cast(userData); - while (!ctx->dirStack.empty() && index >= ctx->dirStack.back().second) { + while (index >= ctx->dirStack.back().second) { ctx->dirStack.pop_back(); } - if (ctx->entries->size() <= index) { - ctx->entries->resize(index + 1); - } + const auto newEntry = std::make_shared( + name, + (kind == NOD_NODE_KIND_DIRECTORY), + size, + index); - FSTEntry& entry = (*ctx->entries)[index]; - entry.name = (name != nullptr) ? name : ""; - entry.isDir = (kind == NOD_NODE_KIND_DIRECTORY); - entry.parent = ctx->dirStack.empty() ? 0 : ctx->dirStack.back().first; - entry.nextOrLength = size; + const auto& curDir = ctx->dirStack.back().first; + curDir->children.push_back(newEntry); - if (entry.isDir) { - ctx->dirStack.emplace_back(index, size); + if (newEntry->isDir) { + ctx->dirStack.emplace_back(newEntry, size); } return index + 1; } +bool nameEqualsIgnoreCase(std::string_view lhs, std::string_view rhs); + +IterateNode* findNode(const IterateNode& node, const std::string_view name) { + for (const auto& child : node.children) { + if (nameEqualsIgnoreCase(child->name, name)) { + return child.get(); + } + } + + return nullptr; +} + +void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFileEntry& overlayFile) { + IterateNode* node = context.root.get(); + std::string_view filePath = overlayFile.fileName; + + assert(filePath.starts_with('/')); + filePath = filePath.substr(1); + while (true) { + const auto nextDelim = filePath.find('/'); + if (nextDelim == std::string_view::npos) { + break; + } + + const auto segment = filePath.substr(0, nextDelim); + filePath = filePath.substr(nextDelim + 1); + + const auto existingNode = findNode(*node, segment); + if (existingNode) { + if (!existingNode->isDir) { + Log.error("Overlay file {} needs directory that's already a file!", overlayFile.fileName); + return; + } + + node = existingNode; + } else { + const auto newNode = std::make_shared(std::string(segment), true, 0, nullptr); + node->children.push_back(newNode); + node = newNode.get(); + } + } + + // Remainder of fileName is the actual file name, and node is the directory we're in. + + auto newNode = IterateNode(std::string(filePath), false, overlayFile.size, overlayFile.userData); + const auto existingNode = findNode(*node, filePath); + if (existingNode) { + if (existingNode->isDir) { + Log.error("Overlay file {} overlaps directory with same name!", overlayFile.fileName); + return; + } + + // Replace existing disc entry. + *existingNode = std::move(newNode); + } else { + // Add new entry. + node->children.emplace_back(std::make_shared(std::move(newNode))); + } +} + +void mergeOverlayFilesIntoContext(const IterateContext& context) { + for (const auto& overlayFile : s_overlayFiles) { + mergeOverlayFileIntoContext(context, overlayFile); + } +} + +void makeFstRecursive(IterateNode& node, u32 parent) { + if (!node.isDir) { + assert(node.children.empty()); + + s_fstEntries.emplace_back(node.name, false, parent, node.size, node.overlayData, node.originalEntryNum); + return; + } + + std::ranges::sort(node.children, [](const auto& a, const auto& b) { return a->name < b->name; }); + + const auto ourIndex = s_fstEntries.size(); + s_fstEntries.emplace_back(node.name, true, parent, 0, node.overlayData, node.originalEntryNum); + + for (const auto& child : node.children) { + makeFstRecursive(*child, ourIndex); + } + + s_fstEntries[ourIndex].nextOrLength = s_fstEntries.size(); +} + +void makeFstFromContext(const IterateContext& context) { + makeFstRecursive(*context.root, 0); +} + bool rebuildFST() { + using namespace std::string_literals; + if (s_partition == nullptr) { return false; } s_fstEntries.clear(); - IterateContext ctx{}; - ctx.entries = &s_fstEntries; - nod_partition_iterate_fst(s_partition, fstCallback, &ctx); + IterateContext ctx; + ctx.root = std::make_shared(""s, true, 0, static_cast(0)); + ctx.dirStack.emplace_back(ctx.root, std::numeric_limits::max()); - if (s_fstEntries.empty()) { - FSTEntry root; - root.name = ""; - root.isDir = true; - root.parent = 0; - root.nextOrLength = 1; - s_fstEntries.push_back(std::move(root)); - } + nod_partition_iterate_fst(s_partition, fstCallback, &ctx); + mergeOverlayFilesIntoContext(ctx); + makeFstFromContext(ctx); - s_fstEntries[0].name.clear(); - s_fstEntries[0].isDir = true; - s_fstEntries[0].parent = 0; - if (s_fstEntries[0].nextOrLength < 1 || s_fstEntries[0].nextOrLength > s_fstEntries.size()) { - s_fstEntries[0].nextOrLength = static_cast(s_fstEntries.size()); - } return true; } -bool nameEqualsIgnoreCase(const std::string& lhs, const char* rhs, size_t rhsLen) { - if (lhs.size() != rhsLen) { +bool nameEqualsIgnoreCase(const std::string_view lhs, const std::string_view rhs) { + if (lhs.size() != rhs.size()) { return false; } - for (size_t i = 0; i < rhsLen; ++i) { + for (size_t i = 0; i < rhs.size(); ++i) { char lc = lhs[i]; char rc = rhs[i]; if (lc >= 'a' && lc <= 'z') { @@ -174,6 +327,10 @@ bool nameEqualsIgnoreCase(const std::string& lhs, const char* rhs, size_t rhsLen return true; } +bool nameEqualsIgnoreCase(const std::string& lhs, const char* rhs, size_t rhsLen) { + return nameEqualsIgnoreCase(lhs, std::string_view(rhs, rhsLen)); +} + s32 findInDir(s32 dirEntry, const char* name, size_t nameLen) { if (!isValidEntryIndex(dirEntry) || !s_fstEntries[dirEntry].isDir) { return -1; @@ -220,7 +377,7 @@ std::string buildDirPath(s32 entryNum) { return out; } -s32 readFromHandle(NodHandle* handle, void* out, s32 length, s32 offset, u32* transferredOut) { +s32 readFromHandle(CommandDataBase* handle, void* out, s32 length, s32 offset, u32* transferredOut) { if (transferredOut != nullptr) { *transferredOut = 0; } @@ -230,7 +387,7 @@ s32 readFromHandle(NodHandle* handle, void* out, s32 length, s32 offset, u32* tr if (length == 0) { return 0; } - if (nod_seek(handle, offset, 0) < 0) { + if (handle->seek(offset, 0) < 0) { return DVD_RESULT_FATAL_ERROR; } @@ -238,7 +395,7 @@ s32 readFromHandle(NodHandle* handle, void* out, s32 length, s32 offset, u32* tr s32 totalRead = 0; s32 remaining = length; while (remaining > 0) { - const int64_t read = nod_read(handle, writePtr + totalRead, static_cast(remaining)); + const int64_t read = handle->read(writePtr + totalRead, static_cast(remaining)); if (read < 0) { return DVD_RESULT_FATAL_ERROR; } @@ -277,9 +434,9 @@ bool isCommandBlockIdle(const DVDCommandBlock* block) { return block != nullptr && block->state != DVD_STATE_BUSY && block->state != DVD_STATE_WAITING; } -NodHandle* getCommandHandle(DVDCommandBlock* block) { +CommandDataBase* getCommandHandle(DVDCommandBlock* block) { if (block != nullptr && block->userData != nullptr) { - return static_cast(block->userData); + return static_cast(block->userData); } return s_disc; } @@ -360,20 +517,23 @@ bool aurora_dvd_open(const char* disc_path) { .preloader_threads = 1, }; - NodResult result = nod_disc_open_stream(&stream, &options, &s_disc); - if (result != NOD_RESULT_OK || s_disc == nullptr) { + NodHandle* discHandle; + NodResult result = nod_disc_open_stream(&stream, &options, &discHandle); + if (result != NOD_RESULT_OK || discHandle == nullptr) { clearState(); return false; } - result = nod_disc_open_partition_kind(s_disc, NOD_PARTITION_KIND_DATA, nullptr, &s_partition); + s_disc = new CommandDataNod(discHandle); + + result = nod_disc_open_partition_kind(s_disc->handle, NOD_PARTITION_KIND_DATA, nullptr, &s_partition); if (result != NOD_RESULT_OK || s_partition == nullptr) { clearState(); return false; } NodDiscHeader header{}; - if (nod_disc_header(s_disc, &header) == NOD_RESULT_OK) { + if (nod_disc_header(s_disc->handle, &header) == NOD_RESULT_OK) { std::memcpy(s_diskID.gameName, header.game_id, sizeof(s_diskID.gameName)); std::memcpy(s_diskID.company, header.game_id + sizeof(s_diskID.gameName), sizeof(s_diskID.company)); s_diskID.diskNumber = header.disc_num; @@ -397,6 +557,42 @@ bool aurora_dvd_open(const char* disc_path) { void aurora_dvd_close(void) { clearState(); } +static bool validateOverlayFile(const AuroraOverlayFile& file) { + const std::string_view name(file.fileName); + + if (!name.starts_with('/')) { + Log.error("Overlay path {} does not start with /", name); + return false; + } + + if (file.size > std::numeric_limits::max()) { + Log.error("Overlay file sizes above 4 GiB are not supported: {}", name); + return false; + } + + return true; +} + +void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles) { + s_overlayFiles.clear(); + + for (size_t i = 0; i < nFiles; i++) { + const auto& file = files[i]; + + if (!validateOverlayFile(file)) { + continue; + } + + s_overlayFiles.emplace_back(file.fileName, file.userData, static_cast(file.size)); + } + + rebuildFST(); +} + +void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks) { + s_overlayCallbacks = *callbacks; +} + void DVDInit(void) {} const u8* DVDGetDOLLocation(s32* out_size) { @@ -443,8 +639,8 @@ int DVDSeekAbsAsyncPrio(DVDCommandBlock* block, s32 offset, DVDCBCallback callba ASSERTMSGLINE(0x7AC, !(offset & (4 - 1)), "DVDSeekAbs(): offset must be a multiple of 4."); beginCommand(block, DVD_COMMAND_SEEK, nullptr, 0, static_cast(offset), callback); - NodHandle* handle = getCommandHandle(block); - const int64_t seek = handle != nullptr ? nod_seek(handle, static_cast(offset), 0) : -1; + auto handle = getCommandHandle(block); + const int64_t seek = handle != nullptr ? handle->seek(static_cast(offset), 0) : -1; const s32 result = (seek < 0) ? DVD_RESULT_FATAL_ERROR : DVD_RESULT_GOOD; finishCommand(block, result, 0); if (callback != nullptr) { @@ -736,21 +932,33 @@ BOOL DVDFastOpen(s32 entrynum, DVDFileInfo* fileInfo) { if (!s_initialized || fileInfo == nullptr || !isValidEntryIndex(entrynum) || s_partition == nullptr) { return FALSE; } - if (s_fstEntries[entrynum].isDir) { + + const auto& entry = s_fstEntries[entrynum]; + if (entry.isDir) { return FALSE; } std::memset(fileInfo, 0, sizeof(*fileInfo)); fileInfo->startAddr = 0; - fileInfo->length = s_fstEntries[entrynum].nextOrLength; + fileInfo->length = entry.nextOrLength; - NodHandle* handle = nullptr; - NodResult result = nod_partition_open_file(s_partition, static_cast(entrynum), &handle); - if (result != NOD_RESULT_OK || handle == nullptr) { - return FALSE; + if (entry.overlayData) { + const auto handle = s_overlayCallbacks.open(entry.overlayData); + if (!handle) { + return FALSE; + } + + fileInfo->cb.userData = new CommandDataOverlay(handle); + } else { + NodHandle* handle = nullptr; + NodResult result = nod_partition_open_file(s_partition, entry.origEntryNum, &handle); + if (result != NOD_RESULT_OK || handle == nullptr) { + return FALSE; + } + + fileInfo->cb.userData = new CommandDataNod(handle); } - fileInfo->cb.userData = handle; fileInfo->cb.state = DVD_STATE_END; return TRUE; } @@ -768,7 +976,7 @@ BOOL DVDClose(DVDFileInfo* fileInfo) { return FALSE; } if (fileInfo->cb.userData != nullptr) { - nod_free(static_cast(fileInfo->cb.userData)); + delete static_cast(fileInfo->cb.userData); fileInfo->cb.userData = nullptr; } fileInfo->cb.state = DVD_STATE_END; @@ -998,7 +1206,7 @@ BOOL DVDLowRead(void* addr, u32 length, u32 offset, DVDLowCallback callback) { } BOOL DVDLowSeek(u32 offset, DVDLowCallback callback) { - const int64_t seek = s_disc != nullptr ? nod_seek(s_disc, static_cast(offset), 0) : -1; + const int64_t seek = s_disc != nullptr ? s_disc->seek(static_cast(offset), 0) : -1; if (callback != nullptr) { callback(static_cast((seek >= 0) ? DVD_RESULT_GOOD : DVD_RESULT_FATAL_ERROR)); } From 675bb40dd30b6b2c0bd99104ffb0b3ad2ca0e081 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Sat, 16 May 2026 00:57:59 +0200 Subject: [PATCH 02/11] Comments and a guard --- include/aurora/dvd.h | 79 +++++++++++++++++++++++++++++++++++++++-- lib/dolphin/dvd/dvd.cpp | 6 ++++ 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/include/aurora/dvd.h b/include/aurora/dvd.h index d4cd9f4f80..84213d7cf2 100644 --- a/include/aurora/dvd.h +++ b/include/aurora/dvd.h @@ -20,23 +20,96 @@ bool aurora_dvd_open(const char* disc_path); */ void aurora_dvd_close(void); +/** + * OVERLAY FILES! + * + * Overlay files allow you to replace and add ("overlay") files that are present in the loaded DVD. + * The way this works is pretty simple: you provide some callbacks and a list of files. + * When an overlaid file gets read, your callbacks get called instead of pulling from the underlying DVD. + * + * Using overlay files results in the DVD EntryNums being observed differently from the original disc. + */ + +/** + * \brief A single file to be overlaid over the DVD files. + * + * You do not need to concern yourself with providing entries for directories. They are automatically merged + * and created where necessary. + */ typedef struct AuroraOverlayFile { + /** + * \brief Absolute file path of this file. + * + * Must be in the form "/foo/bar/baz.txt", note the leading slash. + */ const char* fileName; + + /** + * \brief Userdata pointer that will be passed to the callback when this file is opened. + */ void* userData; + + /** + * \brief Size of this file, in bytes. + * + * While this is of type size_t, file sizes larger than u32 are not currently supported. + */ size_t size; } AuroraOverlayFile; -void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles); - +/** + * \brief Callbacks to implement overlay files. + * + * Callbacks may be ran from any thread at any time. Make sure they're thread safe! + */ typedef struct AuroraOverlayCallbacks { + /** + * Called when a new file has been opened. + * + * Returns an opaque handle that will be passed to the remaining callbacks. Receives the userdata specified in + * the AuroraOverlayFile. + */ void* (*open)(void* userdata); + + /** + * Close a file handle previously returned from the open callback. + */ void (*close)(void* handle); - int64_t (*read)(void* handle, uint8_t *buf, size_t len); + + /** + * Read data from a file handle. + * + * Returns the amount of data read, or -1 on error. + */ + int64_t (*read)(void* handle, uint8_t* buf, size_t len); + + /** + * Seek to a position in a file handle. + * + * Returns the resulting position, or -1 on error. + */ int64_t (*seek)(void* handle, int64_t offset, int32_t whence); } AuroraOverlayCallbacks; +/** + * \brief Specify callbacks for overlaid files. + */ void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks); +/** + * \brief Specify a set of overlay files to be used by the DVD layer. + * + * Calling this function immediately applies the new files and rebuilds the FST. This is not thread safe and will + * invalidate existing EntryNums gotten from the DVD API. It is best you only call this once on startup, + * before the game's code has started. + * + * This function must be called *after* aurora_dvd_overlay_callbacks. + * + * @param files Array of AuroraOverlayFiles, one for every file being overlaid. + * @param nFiles Amount of files in the array. + */ +void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles); + #ifdef __cplusplus } #endif diff --git a/lib/dolphin/dvd/dvd.cpp b/lib/dolphin/dvd/dvd.cpp index 3b3dc08d34..9ce5351245 100644 --- a/lib/dolphin/dvd/dvd.cpp +++ b/lib/dolphin/dvd/dvd.cpp @@ -85,6 +85,7 @@ BOOL s_autoFatalMessaging = FALSE; DVDDiskID s_diskID = {}; DVDLowCallback s_resetCoverCallback = nullptr; bool s_initialized = false; +bool s_overlayCallbacksSet = false; AuroraOverlayCallbacks s_overlayCallbacks; class CommandDataOverlay final : public CommandDataBase { @@ -574,6 +575,10 @@ static bool validateOverlayFile(const AuroraOverlayFile& file) { } void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles) { + if (!s_overlayCallbacksSet) { + Log.fatal("aurora_dvd_overlay_callbacks not called before aurora_dvd_overlay_files!"); + } + s_overlayFiles.clear(); for (size_t i = 0; i < nFiles; i++) { @@ -591,6 +596,7 @@ void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles) { void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks) { s_overlayCallbacks = *callbacks; + s_overlayCallbacksSet = true; } void DVDInit(void) {} From f411b6e3fc306a3be31c517391f7b978694840fa Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Thu, 28 May 2026 00:39:52 +0200 Subject: [PATCH 03/11] EntryNum handling rewrite Game now assigns entrynums for added files, and original disc entrynums are preserved. --- include/aurora/dvd.h | 4 ++ lib/dolphin/dvd/dvd.cpp | 155 ++++++++++++++++++++++++++++++---------- 2 files changed, 123 insertions(+), 36 deletions(-) diff --git a/include/aurora/dvd.h b/include/aurora/dvd.h index 84213d7cf2..c907d755f4 100644 --- a/include/aurora/dvd.h +++ b/include/aurora/dvd.h @@ -55,6 +55,8 @@ typedef struct AuroraOverlayFile { * While this is of type size_t, file sizes larger than u32 are not currently supported. */ size_t size; + + s32 entryNum; } AuroraOverlayFile; /** @@ -110,6 +112,8 @@ void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks); */ void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles); +s32 aurora_dvd_base_entry_count(); + #ifdef __cplusplus } #endif diff --git a/lib/dolphin/dvd/dvd.cpp b/lib/dolphin/dvd/dvd.cpp index 9ce5351245..ae3059da12 100644 --- a/lib/dolphin/dvd/dvd.cpp +++ b/lib/dolphin/dvd/dvd.cpp @@ -21,14 +21,18 @@ namespace { aurora::Module Log("aurora::dvd"); +using PhysicalEntryNum = s32; +using VirtualEntryNum = s32; +constexpr s32 k_invalidFstEntry = -1; + struct FSTEntry { std::string name; bool isDir = false; - u32 parent = 0; + PhysicalEntryNum parent = 0; u32 nextOrLength = 0; void* overlayData = nullptr; // Original entry num on the base game disc, BEFORE being re-organized by overlays. - u32 origEntryNum = 0; + VirtualEntryNum origEntryNum = 0; }; struct IterateNode { @@ -39,8 +43,8 @@ struct IterateNode { void* overlayData; std::vector> children; - IterateNode(std::string name, bool isDir, u32 size, void* overlayData) - : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(0), overlayData(overlayData) {} + IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum, void* overlayData) + : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(overlayData) {} IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum) : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(nullptr) {} @@ -78,7 +82,12 @@ class CommandDataNod final : public CommandDataBase { CommandDataNod* s_disc; NodHandle* s_partition = nullptr; std::vector s_fstEntries; -s32 s_currentDir = 0; +// Map from "virtual" FST entryNums (matching base disc, game-assigned for new overlay files) +// To the "physical" FST entryNums (that we use for navigating the tree). +// Unfilled spots are given the k_invalidFstEntry value. +std::vector s_fstEntryMap; +s32 s_baseEntryCount; +PhysicalEntryNum s_currentDir = 0; std::string s_currentPath = "/"; BOOL s_autoInvalidation = FALSE; BOOL s_autoFatalMessaging = FALSE; @@ -109,6 +118,7 @@ struct OverlayFileEntry { std::string fileName; void* userData; u32 size; + s32 entryNum; }; std::vector s_overlayFiles; @@ -129,7 +139,13 @@ void clearState() { s_initialized = false; } -bool isValidEntryIndex(s32 entry) { return entry >= 0 && static_cast(entry) < s_fstEntries.size(); } +bool isValidVirtualEntry(VirtualEntryNum entry) { + return entry >= 0 && static_cast(entry) < s_fstEntryMap.size() && s_fstEntryMap[entry] != k_invalidFstEntry; +} + +bool isValidPhysicalEntry(PhysicalEntryNum entry) { + return entry >= 0 && static_cast(entry) < s_fstEntries.size(); +} bool isAligned(const void* addr, uintptr_t align) { return (reinterpret_cast(addr) & (align - 1)) == 0; @@ -212,6 +228,14 @@ IterateNode* findNode(const IterateNode& node, const std::string_view name) { } void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFileEntry& overlayFile) { + if (overlayFile.entryNum < s_baseEntryCount) { + Log.error( + "Overlay file {} has entryNum {} which is already used by the base disc!", + overlayFile.fileName, + overlayFile.entryNum); + return; + } + IterateNode* node = context.root.get(); std::string_view filePath = overlayFile.fileName; @@ -235,7 +259,7 @@ void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFil node = existingNode; } else { - const auto newNode = std::make_shared(std::string(segment), true, 0, nullptr); + const auto newNode = std::make_shared(std::string(segment), true, 0, k_invalidFstEntry); node->children.push_back(newNode); node = newNode.get(); } @@ -243,7 +267,7 @@ void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFil // Remainder of fileName is the actual file name, and node is the directory we're in. - auto newNode = IterateNode(std::string(filePath), false, overlayFile.size, overlayFile.userData); + auto newNode = IterateNode(std::string(filePath), false, overlayFile.size, overlayFile.entryNum, overlayFile.userData); const auto existingNode = findNode(*node, filePath); if (existingNode) { if (existingNode->isDir) { @@ -251,10 +275,15 @@ void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFil return; } + newNode.originalEntryNum = existingNode->originalEntryNum; + // Replace existing disc entry. *existingNode = std::move(newNode); } else { // Add new entry. + Log.debug("Adding new entry num: {} -> {}", overlayFile.entryNum, overlayFile.fileName); + newNode.originalEntryNum = overlayFile.entryNum; + node->children.emplace_back(std::make_shared(std::move(newNode))); } } @@ -266,8 +295,23 @@ void mergeOverlayFilesIntoContext(const IterateContext& context) { } void makeFstRecursive(IterateNode& node, u32 parent) { + if (node.originalEntryNum != k_invalidFstEntry) { + if (s_fstEntryMap.size() <= node.originalEntryNum) { + s_fstEntryMap.resize(node.originalEntryNum + 1, k_invalidFstEntry); + } + + auto& map = s_fstEntryMap[node.originalEntryNum]; + if (map != k_invalidFstEntry) { + Log.error("File {} with virtual entry num {} already exists in map!", node.name, node.originalEntryNum); + return; + } + + map = static_cast(s_fstEntries.size()); + } + if (!node.isDir) { assert(node.children.empty()); + assert(node.originalEntryNum != k_invalidFstEntry); s_fstEntries.emplace_back(node.name, false, parent, node.size, node.overlayData, node.originalEntryNum); return; @@ -289,6 +333,16 @@ void makeFstFromContext(const IterateContext& context) { makeFstRecursive(*context.root, 0); } +s32 calcEntryCount(const IterateNode& node) { + s32 counter = 1; + + for (const auto& child : node.children) { + counter += calcEntryCount(*child); + } + + return counter; +} + bool rebuildFST() { using namespace std::string_literals; @@ -296,12 +350,16 @@ bool rebuildFST() { return false; } + // TODO: Ensure current dir still valid after rebuild. + s_fstEntries.clear(); + s_fstEntryMap.clear(); IterateContext ctx; ctx.root = std::make_shared(""s, true, 0, static_cast(0)); ctx.dirStack.emplace_back(ctx.root, std::numeric_limits::max()); nod_partition_iterate_fst(s_partition, fstCallback, &ctx); + s_baseEntryCount = calcEntryCount(*ctx.root); mergeOverlayFilesIntoContext(ctx); makeFstFromContext(ctx); @@ -332,8 +390,8 @@ bool nameEqualsIgnoreCase(const std::string& lhs, const char* rhs, size_t rhsLen return nameEqualsIgnoreCase(lhs, std::string_view(rhs, rhsLen)); } -s32 findInDir(s32 dirEntry, const char* name, size_t nameLen) { - if (!isValidEntryIndex(dirEntry) || !s_fstEntries[dirEntry].isDir) { +PhysicalEntryNum findInDir(PhysicalEntryNum dirEntry, const char* name, size_t nameLen) { + if (!isValidPhysicalEntry(dirEntry) || !s_fstEntries[dirEntry].isDir) { return -1; } @@ -341,7 +399,7 @@ s32 findInDir(s32 dirEntry, const char* name, size_t nameLen) { u32 i = static_cast(dirEntry) + 1; while (i < childEnd && i < s_fstEntries.size()) { if (nameEqualsIgnoreCase(s_fstEntries[i].name, name, nameLen)) { - return static_cast(i); + return static_cast(i); } if (s_fstEntries[i].isDir) { @@ -354,16 +412,16 @@ s32 findInDir(s32 dirEntry, const char* name, size_t nameLen) { return -1; } -std::string buildDirPath(s32 entryNum) { - if (entryNum <= 0 || !isValidEntryIndex(entryNum)) { +std::string buildDirPath(PhysicalEntryNum entryNum) { + if (entryNum <= 0 || !isValidPhysicalEntry(entryNum)) { return "/"; } std::vector parts; - s32 cur = entryNum; - while (cur > 0 && isValidEntryIndex(cur)) { + PhysicalEntryNum cur = entryNum; + while (cur > 0 && isValidPhysicalEntry(cur)) { parts.push_back(s_fstEntries[cur].name); - s32 parent = static_cast(s_fstEntries[cur].parent); + auto parent = s_fstEntries[cur].parent; if (parent == cur) { break; } @@ -571,9 +629,18 @@ static bool validateOverlayFile(const AuroraOverlayFile& file) { return false; } + if (file.entryNum < 0) { + Log.error("Overlay file entry is below zero: {}", name); + return false; + } + return true; } +s32 aurora_dvd_base_entry_count() { + return s_baseEntryCount; +} + void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles) { if (!s_overlayCallbacksSet) { Log.fatal("aurora_dvd_overlay_callbacks not called before aurora_dvd_overlay_files!"); @@ -588,7 +655,7 @@ void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles) { continue; } - s_overlayFiles.emplace_back(file.fileName, file.userData, static_cast(file.size)); + s_overlayFiles.emplace_back(file.fileName, file.userData, static_cast(file.size), file.entryNum); } rebuildFST(); @@ -886,12 +953,12 @@ int DVDSetAutoFatalMessaging(BOOL enable) { return prev; } -s32 DVDConvertPathToEntrynum(const char* pathPtr) { +VirtualEntryNum DVDConvertPathToEntrynum(const char* pathPtr) { if (!s_initialized || pathPtr == nullptr || s_fstEntries.empty()) { return -1; } - s32 current = 0; + PhysicalEntryNum current = 0; const char* p = pathPtr; if (*p == '/') { ++p; @@ -907,7 +974,7 @@ s32 DVDConvertPathToEntrynum(const char* pathPtr) { break; } - if (!isValidEntryIndex(current) || !s_fstEntries[current].isDir) { + if (!isValidPhysicalEntry(current) || !s_fstEntries[current].isDir) { return -1; } @@ -922,7 +989,7 @@ s32 DVDConvertPathToEntrynum(const char* pathPtr) { } else if (compLen == 2 && p[0] == '.' && p[1] == '.') { current = static_cast(s_fstEntries[current].parent); } else { - const s32 found = findInDir(current, p, compLen); + const PhysicalEntryNum found = findInDir(current, p, compLen); if (found < 0) { return -1; } @@ -931,15 +998,19 @@ s32 DVDConvertPathToEntrynum(const char* pathPtr) { p = compEnd; } - return current; + assert(isValidPhysicalEntry(current)); + return s_fstEntries[current].origEntryNum; } -BOOL DVDFastOpen(s32 entrynum, DVDFileInfo* fileInfo) { - if (!s_initialized || fileInfo == nullptr || !isValidEntryIndex(entrynum) || s_partition == nullptr) { +BOOL DVDFastOpen(VirtualEntryNum entrynum, DVDFileInfo* fileInfo) { + if (!s_initialized || fileInfo == nullptr || !isValidVirtualEntry(entrynum) || s_partition == nullptr) { return FALSE; } - const auto& entry = s_fstEntries[entrynum]; + const auto physical = s_fstEntryMap[entrynum]; + assert(physical >= 0); + + const auto& entry = s_fstEntries[physical]; if (entry.isDir) { return FALSE; } @@ -970,7 +1041,7 @@ BOOL DVDFastOpen(s32 entrynum, DVDFileInfo* fileInfo) { } BOOL DVDOpen(const char* fileName, DVDFileInfo* fileInfo) { - s32 entrynum = DVDConvertPathToEntrynum(fileName); + VirtualEntryNum entrynum = DVDConvertPathToEntrynum(fileName); if (entrynum < 0) { return FALSE; } @@ -1001,12 +1072,18 @@ BOOL DVDGetCurrentDir(char* path, u32 maxlen) { } BOOL DVDChangeDir(const char* dirName) { - s32 entry = DVDConvertPathToEntrynum(dirName); - if (!isValidEntryIndex(entry) || !s_fstEntries[entry].isDir) { + VirtualEntryNum entry = DVDConvertPathToEntrynum(dirName); + if (!isValidVirtualEntry(entry)) { return FALSE; } - s_currentDir = entry; - s_currentPath = buildDirPath(entry); + + const auto physical = s_fstEntryMap[entry]; + if (!s_fstEntries[physical].isDir) { + return FALSE; + } + + s_currentDir = physical; + s_currentPath = buildDirPath(physical); return TRUE; } @@ -1069,18 +1146,24 @@ s32 DVDGetFileInfoStatus(const DVDFileInfo* fileInfo) { return fileInfo->cb.state; } -BOOL DVDFastOpenDir(s32 entrynum, DVDDir* dir) { - if (!isValidEntryIndex(entrynum) || dir == nullptr || !s_fstEntries[entrynum].isDir) { +BOOL DVDFastOpenDir(VirtualEntryNum entrynum, DVDDir* dir) { + if (!isValidVirtualEntry(entrynum) || dir == nullptr) { return FALSE; } - dir->entryNum = static_cast(entrynum); - dir->location = static_cast(entrynum) + 1; - dir->next = s_fstEntries[entrynum].nextOrLength; + + const auto physical = s_fstEntryMap[entrynum]; + if (!s_fstEntries[physical].isDir) { + return FALSE; + } + + dir->entryNum = static_cast(physical); + dir->location = static_cast(physical) + 1; + dir->next = s_fstEntries[physical].nextOrLength; return TRUE; } int DVDOpenDir(const char* dirName, DVDDir* dir) { - s32 entrynum = DVDConvertPathToEntrynum(dirName); + VirtualEntryNum entrynum = DVDConvertPathToEntrynum(dirName); if (entrynum < 0) { return FALSE; } From 6e76b818e195d32cbd4f965d2686972d86a74d81 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Thu, 28 May 2026 22:02:03 +0200 Subject: [PATCH 04/11] Split FST logic into another file in dvd lib --- cmake/aurora_dvd.cmake | 2 +- lib/dolphin/dvd/dvd.cpp | 332 +++------------------------------------- lib/dolphin/dvd/dvd.hpp | 72 +++++++++ lib/dolphin/dvd/fst.cpp | 265 ++++++++++++++++++++++++++++++++ 4 files changed, 363 insertions(+), 308 deletions(-) create mode 100644 lib/dolphin/dvd/dvd.hpp create mode 100644 lib/dolphin/dvd/fst.cpp diff --git a/cmake/aurora_dvd.cmake b/cmake/aurora_dvd.cmake index c95f72e5fe..35e0c81273 100644 --- a/cmake/aurora_dvd.cmake +++ b/cmake/aurora_dvd.cmake @@ -1,6 +1,6 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/AuroraNodProvider.cmake) -add_library(aurora_dvd STATIC lib/dolphin/dvd/dvd.cpp) +add_library(aurora_dvd STATIC lib/dolphin/dvd/dvd.cpp lib/dolphin/dvd/dvd.hpp lib/dolphin/dvd/fst.cpp) add_library(aurora::dvd ALIAS aurora_dvd) set_target_properties(aurora_dvd PROPERTIES FOLDER "aurora") diff --git a/lib/dolphin/dvd/dvd.cpp b/lib/dolphin/dvd/dvd.cpp index ae3059da12..70ae6919a5 100644 --- a/lib/dolphin/dvd/dvd.cpp +++ b/lib/dolphin/dvd/dvd.cpp @@ -15,45 +15,32 @@ #include #include -#include "../../internal.hpp" - -namespace { - -aurora::Module Log("aurora::dvd"); - -using PhysicalEntryNum = s32; -using VirtualEntryNum = s32; -constexpr s32 k_invalidFstEntry = -1; +#include "dvd.hpp" -struct FSTEntry { - std::string name; - bool isDir = false; - PhysicalEntryNum parent = 0; - u32 nextOrLength = 0; - void* overlayData = nullptr; - // Original entry num on the base game disc, BEFORE being re-organized by overlays. - VirtualEntryNum origEntryNum = 0; -}; - -struct IterateNode { - std::string name; - bool isDir; - u32 originalEntryNum; - u32 size; - void* overlayData; - std::vector> children; - - IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum, void* overlayData) - : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(overlayData) {} +#include "../../internal.hpp" - IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum) - : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(nullptr) {} -}; +using namespace aurora::dvd::impl; + +namespace aurora::dvd::impl { + NodHandle* s_partition = nullptr; + std::vector s_fstEntries; + // Map from "virtual" FST entryNums (matching base disc, game-assigned for new overlay files) + // To the "physical" FST entryNums (that we use for navigating the tree). + // Unfilled spots are given the k_invalidFstEntry value. + std::vector s_fstEntryMap; + s32 s_baseEntryCount; + PhysicalEntryNum s_currentDir = 0; + std::string s_currentPath = "/"; + BOOL s_autoInvalidation = FALSE; + BOOL s_autoFatalMessaging = FALSE; + DVDDiskID s_diskID = {}; + DVDLowCallback s_resetCoverCallback = nullptr; + bool s_initialized = false; + bool s_overlayCallbacksSet = false; + AuroraOverlayCallbacks s_overlayCallbacks; +} -struct IterateContext { - std::shared_ptr root; - std::vector, u32>> dirStack; -}; +namespace { class CommandDataBase { public: @@ -79,24 +66,6 @@ class CommandDataNod final : public CommandDataBase { } }; -CommandDataNod* s_disc; -NodHandle* s_partition = nullptr; -std::vector s_fstEntries; -// Map from "virtual" FST entryNums (matching base disc, game-assigned for new overlay files) -// To the "physical" FST entryNums (that we use for navigating the tree). -// Unfilled spots are given the k_invalidFstEntry value. -std::vector s_fstEntryMap; -s32 s_baseEntryCount; -PhysicalEntryNum s_currentDir = 0; -std::string s_currentPath = "/"; -BOOL s_autoInvalidation = FALSE; -BOOL s_autoFatalMessaging = FALSE; -DVDDiskID s_diskID = {}; -DVDLowCallback s_resetCoverCallback = nullptr; -bool s_initialized = false; -bool s_overlayCallbacksSet = false; -AuroraOverlayCallbacks s_overlayCallbacks; - class CommandDataOverlay final : public CommandDataBase { public: void* handle; @@ -114,14 +83,7 @@ class CommandDataOverlay final : public CommandDataBase { } }; -struct OverlayFileEntry { - std::string fileName; - void* userData; - u32 size; - s32 entryNum; -}; - -std::vector s_overlayFiles; +CommandDataNod* s_disc; void clearState() { if (s_partition != nullptr) { @@ -192,202 +154,8 @@ void sdlStreamClose(void* userData) { SDL_CloseIO(io); } -u32 fstCallback(u32 index, NodNodeKind kind, const char* name, u32 size, void* userData) { - auto* ctx = static_cast(userData); - - while (index >= ctx->dirStack.back().second) { - ctx->dirStack.pop_back(); - } - - const auto newEntry = std::make_shared( - name, - (kind == NOD_NODE_KIND_DIRECTORY), - size, - index); - - const auto& curDir = ctx->dirStack.back().first; - curDir->children.push_back(newEntry); - - if (newEntry->isDir) { - ctx->dirStack.emplace_back(newEntry, size); - } - - return index + 1; -} - -bool nameEqualsIgnoreCase(std::string_view lhs, std::string_view rhs); - -IterateNode* findNode(const IterateNode& node, const std::string_view name) { - for (const auto& child : node.children) { - if (nameEqualsIgnoreCase(child->name, name)) { - return child.get(); - } - } - - return nullptr; -} - -void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFileEntry& overlayFile) { - if (overlayFile.entryNum < s_baseEntryCount) { - Log.error( - "Overlay file {} has entryNum {} which is already used by the base disc!", - overlayFile.fileName, - overlayFile.entryNum); - return; - } - - IterateNode* node = context.root.get(); - std::string_view filePath = overlayFile.fileName; - - assert(filePath.starts_with('/')); - filePath = filePath.substr(1); - while (true) { - const auto nextDelim = filePath.find('/'); - if (nextDelim == std::string_view::npos) { - break; - } - - const auto segment = filePath.substr(0, nextDelim); - filePath = filePath.substr(nextDelim + 1); - - const auto existingNode = findNode(*node, segment); - if (existingNode) { - if (!existingNode->isDir) { - Log.error("Overlay file {} needs directory that's already a file!", overlayFile.fileName); - return; - } - - node = existingNode; - } else { - const auto newNode = std::make_shared(std::string(segment), true, 0, k_invalidFstEntry); - node->children.push_back(newNode); - node = newNode.get(); - } - } - - // Remainder of fileName is the actual file name, and node is the directory we're in. - - auto newNode = IterateNode(std::string(filePath), false, overlayFile.size, overlayFile.entryNum, overlayFile.userData); - const auto existingNode = findNode(*node, filePath); - if (existingNode) { - if (existingNode->isDir) { - Log.error("Overlay file {} overlaps directory with same name!", overlayFile.fileName); - return; - } - - newNode.originalEntryNum = existingNode->originalEntryNum; - - // Replace existing disc entry. - *existingNode = std::move(newNode); - } else { - // Add new entry. - Log.debug("Adding new entry num: {} -> {}", overlayFile.entryNum, overlayFile.fileName); - newNode.originalEntryNum = overlayFile.entryNum; - - node->children.emplace_back(std::make_shared(std::move(newNode))); - } -} - -void mergeOverlayFilesIntoContext(const IterateContext& context) { - for (const auto& overlayFile : s_overlayFiles) { - mergeOverlayFileIntoContext(context, overlayFile); - } -} - -void makeFstRecursive(IterateNode& node, u32 parent) { - if (node.originalEntryNum != k_invalidFstEntry) { - if (s_fstEntryMap.size() <= node.originalEntryNum) { - s_fstEntryMap.resize(node.originalEntryNum + 1, k_invalidFstEntry); - } - - auto& map = s_fstEntryMap[node.originalEntryNum]; - if (map != k_invalidFstEntry) { - Log.error("File {} with virtual entry num {} already exists in map!", node.name, node.originalEntryNum); - return; - } - - map = static_cast(s_fstEntries.size()); - } - - if (!node.isDir) { - assert(node.children.empty()); - assert(node.originalEntryNum != k_invalidFstEntry); - - s_fstEntries.emplace_back(node.name, false, parent, node.size, node.overlayData, node.originalEntryNum); - return; - } - - std::ranges::sort(node.children, [](const auto& a, const auto& b) { return a->name < b->name; }); - - const auto ourIndex = s_fstEntries.size(); - s_fstEntries.emplace_back(node.name, true, parent, 0, node.overlayData, node.originalEntryNum); - - for (const auto& child : node.children) { - makeFstRecursive(*child, ourIndex); - } - - s_fstEntries[ourIndex].nextOrLength = s_fstEntries.size(); -} - -void makeFstFromContext(const IterateContext& context) { - makeFstRecursive(*context.root, 0); -} - -s32 calcEntryCount(const IterateNode& node) { - s32 counter = 1; - - for (const auto& child : node.children) { - counter += calcEntryCount(*child); - } - - return counter; -} - -bool rebuildFST() { - using namespace std::string_literals; - - if (s_partition == nullptr) { - return false; - } - - // TODO: Ensure current dir still valid after rebuild. - - s_fstEntries.clear(); - s_fstEntryMap.clear(); - IterateContext ctx; - ctx.root = std::make_shared(""s, true, 0, static_cast(0)); - ctx.dirStack.emplace_back(ctx.root, std::numeric_limits::max()); - - nod_partition_iterate_fst(s_partition, fstCallback, &ctx); - s_baseEntryCount = calcEntryCount(*ctx.root); - mergeOverlayFilesIntoContext(ctx); - makeFstFromContext(ctx); - - return true; -} - -bool nameEqualsIgnoreCase(const std::string_view lhs, const std::string_view rhs) { - if (lhs.size() != rhs.size()) { - return false; - } - for (size_t i = 0; i < rhs.size(); ++i) { - char lc = lhs[i]; - char rc = rhs[i]; - if (lc >= 'a' && lc <= 'z') { - lc = static_cast(lc - 'a' + 'A'); - } - if (rc >= 'a' && rc <= 'z') { - rc = static_cast(rc - 'a' + 'A'); - } - if (lc != rc) { - return false; - } - } - return true; -} - bool nameEqualsIgnoreCase(const std::string& lhs, const char* rhs, size_t rhsLen) { - return nameEqualsIgnoreCase(lhs, std::string_view(rhs, rhsLen)); + return aurora::dvd::impl::nameEqualsIgnoreCase(lhs, std::string_view(rhs, rhsLen)); } PhysicalEntryNum findInDir(PhysicalEntryNum dirEntry, const char* name, size_t nameLen) { @@ -616,56 +384,6 @@ bool aurora_dvd_open(const char* disc_path) { void aurora_dvd_close(void) { clearState(); } -static bool validateOverlayFile(const AuroraOverlayFile& file) { - const std::string_view name(file.fileName); - - if (!name.starts_with('/')) { - Log.error("Overlay path {} does not start with /", name); - return false; - } - - if (file.size > std::numeric_limits::max()) { - Log.error("Overlay file sizes above 4 GiB are not supported: {}", name); - return false; - } - - if (file.entryNum < 0) { - Log.error("Overlay file entry is below zero: {}", name); - return false; - } - - return true; -} - -s32 aurora_dvd_base_entry_count() { - return s_baseEntryCount; -} - -void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles) { - if (!s_overlayCallbacksSet) { - Log.fatal("aurora_dvd_overlay_callbacks not called before aurora_dvd_overlay_files!"); - } - - s_overlayFiles.clear(); - - for (size_t i = 0; i < nFiles; i++) { - const auto& file = files[i]; - - if (!validateOverlayFile(file)) { - continue; - } - - s_overlayFiles.emplace_back(file.fileName, file.userData, static_cast(file.size), file.entryNum); - } - - rebuildFST(); -} - -void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks) { - s_overlayCallbacks = *callbacks; - s_overlayCallbacksSet = true; -} - void DVDInit(void) {} const u8* DVDGetDOLLocation(s32* out_size) { diff --git a/lib/dolphin/dvd/dvd.hpp b/lib/dolphin/dvd/dvd.hpp new file mode 100644 index 0000000000..0e613c9202 --- /dev/null +++ b/lib/dolphin/dvd/dvd.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +#include "../../internal.hpp" + +namespace aurora::dvd::impl { + +inline Module Log("aurora::dvd"); + +using PhysicalEntryNum = s32; +using VirtualEntryNum = s32; +constexpr s32 k_invalidFstEntry = -1; + +struct FSTEntry { + std::string name; + bool isDir = false; + PhysicalEntryNum parent = 0; + u32 nextOrLength = 0; + void* overlayData = nullptr; + // Original entry num on the base game disc, BEFORE being re-organized by overlays. + VirtualEntryNum origEntryNum = 0; +}; + +struct IterateNode { + std::string name; + bool isDir; + u32 originalEntryNum; + u32 size; + void* overlayData; + std::vector> children; + + IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum, void* overlayData) + : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(overlayData) {} + + IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum) + : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(nullptr) {} +}; + +struct IterateContext { + std::shared_ptr root; + std::vector, u32>> dirStack; +}; + +extern NodHandle* s_partition; +extern std::vector s_fstEntries; +// Map from "virtual" FST entryNums (matching base disc, game-assigned for new overlay files) +// To the "physical" FST entryNums (that we use for navigating the tree). +// Unfilled spots are given the k_invalidFstEntry value. +extern std::vector s_fstEntryMap; +extern s32 s_baseEntryCount; +extern PhysicalEntryNum s_currentDir; +extern std::string s_currentPath; +extern BOOL s_autoInvalidation; +extern BOOL s_autoFatalMessaging; +extern DVDDiskID s_diskID; +extern DVDLowCallback s_resetCoverCallback; +extern bool s_initialized; +extern bool s_overlayCallbacksSet; +extern AuroraOverlayCallbacks s_overlayCallbacks; + +bool rebuildFST(); +bool nameEqualsIgnoreCase(std::string_view lhs, std::string_view rhs); + +} \ No newline at end of file diff --git a/lib/dolphin/dvd/fst.cpp b/lib/dolphin/dvd/fst.cpp new file mode 100644 index 0000000000..0b78591b50 --- /dev/null +++ b/lib/dolphin/dvd/fst.cpp @@ -0,0 +1,265 @@ +#include "dvd.hpp" + +#include + +using namespace aurora::dvd::impl; + +namespace { + +struct OverlayFileEntry { + std::string fileName; + void* userData; + u32 size; + s32 entryNum; +}; + +std::vector s_overlayFiles; + + +u32 fstCallback(u32 index, NodNodeKind kind, const char* name, u32 size, void* userData) { + auto* ctx = static_cast(userData); + + while (index >= ctx->dirStack.back().second) { + ctx->dirStack.pop_back(); + } + + const auto newEntry = std::make_shared( + name, + (kind == NOD_NODE_KIND_DIRECTORY), + size, + index); + + const auto& curDir = ctx->dirStack.back().first; + curDir->children.push_back(newEntry); + + if (newEntry->isDir) { + ctx->dirStack.emplace_back(newEntry, size); + } + + return index + 1; +} + +IterateNode* findNode(const IterateNode& node, const std::string_view name) { + for (const auto& child : node.children) { + if (nameEqualsIgnoreCase(child->name, name)) { + return child.get(); + } + } + + return nullptr; +} + +void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFileEntry& overlayFile) { + if (overlayFile.entryNum < s_baseEntryCount) { + Log.error( + "Overlay file {} has entryNum {} which is already used by the base disc!", + overlayFile.fileName, + overlayFile.entryNum); + return; + } + + IterateNode* node = context.root.get(); + std::string_view filePath = overlayFile.fileName; + + assert(filePath.starts_with('/')); + filePath = filePath.substr(1); + while (true) { + const auto nextDelim = filePath.find('/'); + if (nextDelim == std::string_view::npos) { + break; + } + + const auto segment = filePath.substr(0, nextDelim); + filePath = filePath.substr(nextDelim + 1); + + const auto existingNode = findNode(*node, segment); + if (existingNode) { + if (!existingNode->isDir) { + Log.error("Overlay file {} needs directory that's already a file!", overlayFile.fileName); + return; + } + + node = existingNode; + } else { + const auto newNode = std::make_shared(std::string(segment), true, 0, k_invalidFstEntry); + node->children.push_back(newNode); + node = newNode.get(); + } + } + + // Remainder of fileName is the actual file name, and node is the directory we're in. + + auto newNode = IterateNode(std::string(filePath), false, overlayFile.size, overlayFile.entryNum, overlayFile.userData); + const auto existingNode = findNode(*node, filePath); + if (existingNode) { + if (existingNode->isDir) { + Log.error("Overlay file {} overlaps directory with same name!", overlayFile.fileName); + return; + } + + newNode.originalEntryNum = existingNode->originalEntryNum; + + // Replace existing disc entry. + *existingNode = std::move(newNode); + } else { + // Add new entry. + Log.debug("Adding new entry num: {} -> {}", overlayFile.entryNum, overlayFile.fileName); + newNode.originalEntryNum = overlayFile.entryNum; + + node->children.emplace_back(std::make_shared(std::move(newNode))); + } +} + +void mergeOverlayFilesIntoContext(const IterateContext& context) { + for (const auto& overlayFile : s_overlayFiles) { + mergeOverlayFileIntoContext(context, overlayFile); + } +} + +void makeFstRecursive(IterateNode& node, u32 parent) { + if (node.originalEntryNum != k_invalidFstEntry) { + if (s_fstEntryMap.size() <= node.originalEntryNum) { + s_fstEntryMap.resize(node.originalEntryNum + 1, k_invalidFstEntry); + } + + auto& map = s_fstEntryMap[node.originalEntryNum]; + if (map != k_invalidFstEntry) { + Log.error("File {} with virtual entry num {} already exists in map!", node.name, node.originalEntryNum); + return; + } + + map = static_cast(s_fstEntries.size()); + } + + if (!node.isDir) { + assert(node.children.empty()); + assert(node.originalEntryNum != k_invalidFstEntry); + + s_fstEntries.emplace_back(node.name, false, parent, node.size, node.overlayData, node.originalEntryNum); + return; + } + + std::ranges::sort(node.children, [](const auto& a, const auto& b) { return a->name < b->name; }); + + const auto ourIndex = s_fstEntries.size(); + s_fstEntries.emplace_back(node.name, true, parent, 0, node.overlayData, node.originalEntryNum); + + for (const auto& child : node.children) { + makeFstRecursive(*child, ourIndex); + } + + s_fstEntries[ourIndex].nextOrLength = s_fstEntries.size(); +} + +void makeFstFromContext(const IterateContext& context) { + makeFstRecursive(*context.root, 0); +} + +s32 calcEntryCount(const IterateNode& node) { + s32 counter = 1; + + for (const auto& child : node.children) { + counter += calcEntryCount(*child); + } + + return counter; +} + +bool validateOverlayFile(const AuroraOverlayFile& file) { + const std::string_view name(file.fileName); + + if (!name.starts_with('/')) { + Log.error("Overlay path {} does not start with /", name); + return false; + } + + if (file.size > std::numeric_limits::max()) { + Log.error("Overlay file sizes above 4 GiB are not supported: {}", name); + return false; + } + + if (file.entryNum < 0) { + Log.error("Overlay file entry is below zero: {}", name); + return false; + } + + return true; +} + +} + +namespace aurora::dvd::impl { + +bool rebuildFST() { + using namespace std::string_literals; + + if (s_partition == nullptr) { + return false; + } + + // TODO: Ensure current dir still valid after rebuild. + + s_fstEntries.clear(); + s_fstEntryMap.clear(); + IterateContext ctx; + ctx.root = std::make_shared(""s, true, 0, static_cast(0)); + ctx.dirStack.emplace_back(ctx.root, std::numeric_limits::max()); + + nod_partition_iterate_fst(s_partition, fstCallback, &ctx); + s_baseEntryCount = calcEntryCount(*ctx.root); + mergeOverlayFilesIntoContext(ctx); + makeFstFromContext(ctx); + + return true; +} + +bool nameEqualsIgnoreCase(const std::string_view lhs, const std::string_view rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + for (size_t i = 0; i < rhs.size(); ++i) { + char lc = lhs[i]; + char rc = rhs[i]; + if (lc >= 'a' && lc <= 'z') { + lc = static_cast(lc - 'a' + 'A'); + } + if (rc >= 'a' && rc <= 'z') { + rc = static_cast(rc - 'a' + 'A'); + } + if (lc != rc) { + return false; + } + } + return true; +} + +} + +s32 aurora_dvd_base_entry_count() { + return s_baseEntryCount; +} + +void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles) { + if (!s_overlayCallbacksSet) { + Log.fatal("aurora_dvd_overlay_callbacks not called before aurora_dvd_overlay_files!"); + } + + s_overlayFiles.clear(); + + for (size_t i = 0; i < nFiles; i++) { + const auto& file = files[i]; + + if (!validateOverlayFile(file)) { + continue; + } + + s_overlayFiles.emplace_back(file.fileName, file.userData, static_cast(file.size), file.entryNum); + } + + rebuildFST(); +} + +void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks) { + s_overlayCallbacks = *callbacks; + s_overlayCallbacksSet = true; +} From 8c5c16e31ae83588d73a9504b28e571ffb740ca3 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 29 May 2026 10:09:44 +0200 Subject: [PATCH 05/11] Fix overlay userdata 0 Dusklight was using integer indexes for the userdata of overlay files. This meant file 0 was a null void*, and that meant the DVD code didn't treat it as an overlaid file. Add an extra bool to avoid getting confused like this. --- lib/dolphin/dvd/dvd.cpp | 2 +- lib/dolphin/dvd/dvd.hpp | 7 ++++--- lib/dolphin/dvd/fst.cpp | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/dolphin/dvd/dvd.cpp b/lib/dolphin/dvd/dvd.cpp index 70ae6919a5..0259e9661a 100644 --- a/lib/dolphin/dvd/dvd.cpp +++ b/lib/dolphin/dvd/dvd.cpp @@ -737,7 +737,7 @@ BOOL DVDFastOpen(VirtualEntryNum entrynum, DVDFileInfo* fileInfo) { fileInfo->startAddr = 0; fileInfo->length = entry.nextOrLength; - if (entry.overlayData) { + if (entry.isOverlay) { const auto handle = s_overlayCallbacks.open(entry.overlayData); if (!handle) { return FALSE; diff --git a/lib/dolphin/dvd/dvd.hpp b/lib/dolphin/dvd/dvd.hpp index 0e613c9202..52362ab4aa 100644 --- a/lib/dolphin/dvd/dvd.hpp +++ b/lib/dolphin/dvd/dvd.hpp @@ -25,7 +25,7 @@ struct FSTEntry { PhysicalEntryNum parent = 0; u32 nextOrLength = 0; void* overlayData = nullptr; - // Original entry num on the base game disc, BEFORE being re-organized by overlays. + bool isOverlay = false; VirtualEntryNum origEntryNum = 0; }; @@ -35,13 +35,14 @@ struct IterateNode { u32 originalEntryNum; u32 size; void* overlayData; + bool isOverlay; std::vector> children; IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum, void* overlayData) - : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(overlayData) {} + : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(overlayData), isOverlay(true) {} IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum) - : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(nullptr) {} + : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(nullptr), isOverlay(false) {} }; struct IterateContext { diff --git a/lib/dolphin/dvd/fst.cpp b/lib/dolphin/dvd/fst.cpp index 0b78591b50..25c141be73 100644 --- a/lib/dolphin/dvd/fst.cpp +++ b/lib/dolphin/dvd/fst.cpp @@ -135,14 +135,14 @@ void makeFstRecursive(IterateNode& node, u32 parent) { assert(node.children.empty()); assert(node.originalEntryNum != k_invalidFstEntry); - s_fstEntries.emplace_back(node.name, false, parent, node.size, node.overlayData, node.originalEntryNum); + s_fstEntries.emplace_back(node.name, false, parent, node.size, node.overlayData, node.isOverlay, node.originalEntryNum); return; } std::ranges::sort(node.children, [](const auto& a, const auto& b) { return a->name < b->name; }); const auto ourIndex = s_fstEntries.size(); - s_fstEntries.emplace_back(node.name, true, parent, 0, node.overlayData, node.originalEntryNum); + s_fstEntries.emplace_back(node.name, true, parent, 0, node.overlayData, node.isOverlay, node.originalEntryNum); for (const auto& child : node.children) { makeFstRecursive(*child, ourIndex); From e2254ae3b79034a00da63e9f4b1fb990329c2aea Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 29 May 2026 10:38:32 +0200 Subject: [PATCH 06/11] Make entryNum only validated if not replacing file --- lib/dolphin/dvd/fst.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/dolphin/dvd/fst.cpp b/lib/dolphin/dvd/fst.cpp index 25c141be73..57a4583468 100644 --- a/lib/dolphin/dvd/fst.cpp +++ b/lib/dolphin/dvd/fst.cpp @@ -50,14 +50,6 @@ IterateNode* findNode(const IterateNode& node, const std::string_view name) { } void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFileEntry& overlayFile) { - if (overlayFile.entryNum < s_baseEntryCount) { - Log.error( - "Overlay file {} has entryNum {} which is already used by the base disc!", - overlayFile.fileName, - overlayFile.entryNum); - return; - } - IterateNode* node = context.root.get(); std::string_view filePath = overlayFile.fileName; @@ -103,6 +95,14 @@ void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFil *existingNode = std::move(newNode); } else { // Add new entry. + if (overlayFile.entryNum < s_baseEntryCount) { + Log.error( + "Overlay file {} has entryNum {} which is already used by the base disc!", + overlayFile.fileName, + overlayFile.entryNum); + return; + } + Log.debug("Adding new entry num: {} -> {}", overlayFile.entryNum, overlayFile.fileName); newNode.originalEntryNum = overlayFile.entryNum; From cf9cdaf65efefb796766c7d9145ada6b61e0d0ec Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 29 May 2026 11:09:44 +0200 Subject: [PATCH 07/11] Make FST rebuilds thread safe with concurrent readers Since I kinda would like to eventually allow mods to be loaded without game restart (where practical) --- lib/dolphin/dvd/dvd.cpp | 13 +++++++++++++ lib/dolphin/dvd/dvd.hpp | 2 ++ lib/dolphin/dvd/fst.cpp | 2 ++ 3 files changed, 17 insertions(+) diff --git a/lib/dolphin/dvd/dvd.cpp b/lib/dolphin/dvd/dvd.cpp index 0259e9661a..9747737b19 100644 --- a/lib/dolphin/dvd/dvd.cpp +++ b/lib/dolphin/dvd/dvd.cpp @@ -38,6 +38,7 @@ namespace aurora::dvd::impl { bool s_initialized = false; bool s_overlayCallbacksSet = false; AuroraOverlayCallbacks s_overlayCallbacks; + std::mutex s_fstLock; } namespace { @@ -672,6 +673,8 @@ int DVDSetAutoFatalMessaging(BOOL enable) { } VirtualEntryNum DVDConvertPathToEntrynum(const char* pathPtr) { + std::lock_guard lock(s_fstLock); + if (!s_initialized || pathPtr == nullptr || s_fstEntries.empty()) { return -1; } @@ -721,6 +724,8 @@ VirtualEntryNum DVDConvertPathToEntrynum(const char* pathPtr) { } BOOL DVDFastOpen(VirtualEntryNum entrynum, DVDFileInfo* fileInfo) { + std::lock_guard lock(s_fstLock); + if (!s_initialized || fileInfo == nullptr || !isValidVirtualEntry(entrynum) || s_partition == nullptr) { return FALSE; } @@ -791,6 +796,9 @@ BOOL DVDGetCurrentDir(char* path, u32 maxlen) { BOOL DVDChangeDir(const char* dirName) { VirtualEntryNum entry = DVDConvertPathToEntrynum(dirName); + + std::lock_guard lock(s_fstLock); + if (!isValidVirtualEntry(entry)) { return FALSE; } @@ -865,6 +873,8 @@ s32 DVDGetFileInfoStatus(const DVDFileInfo* fileInfo) { } BOOL DVDFastOpenDir(VirtualEntryNum entrynum, DVDDir* dir) { + std::lock_guard lock(s_fstLock); + if (!isValidVirtualEntry(entrynum) || dir == nullptr) { return FALSE; } @@ -892,6 +902,9 @@ int DVDReadDir(DVDDir* dir, DVDDirEntry* dirent) { if (dir == nullptr || dirent == nullptr) { return FALSE; } + + std::lock_guard lock(s_fstLock); + if (dir->location >= dir->next || dir->location >= s_fstEntries.size()) { return FALSE; } diff --git a/lib/dolphin/dvd/dvd.hpp b/lib/dolphin/dvd/dvd.hpp index 52362ab4aa..435d4c4377 100644 --- a/lib/dolphin/dvd/dvd.hpp +++ b/lib/dolphin/dvd/dvd.hpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -66,6 +67,7 @@ extern DVDLowCallback s_resetCoverCallback; extern bool s_initialized; extern bool s_overlayCallbacksSet; extern AuroraOverlayCallbacks s_overlayCallbacks; +extern std::mutex s_fstLock; bool rebuildFST(); bool nameEqualsIgnoreCase(std::string_view lhs, std::string_view rhs); diff --git a/lib/dolphin/dvd/fst.cpp b/lib/dolphin/dvd/fst.cpp index 57a4583468..91845ac740 100644 --- a/lib/dolphin/dvd/fst.cpp +++ b/lib/dolphin/dvd/fst.cpp @@ -197,6 +197,8 @@ bool rebuildFST() { return false; } + std::lock_guard lock(s_fstLock); + // TODO: Ensure current dir still valid after rebuild. s_fstEntries.clear(); From dfd3f60aadb4908214e3657508f0f3dfa308e175 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 29 May 2026 11:11:51 +0200 Subject: [PATCH 08/11] Doc updates --- include/aurora/dvd.h | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/include/aurora/dvd.h b/include/aurora/dvd.h index c907d755f4..5fdac71e52 100644 --- a/include/aurora/dvd.h +++ b/include/aurora/dvd.h @@ -27,7 +27,8 @@ void aurora_dvd_close(void); * The way this works is pretty simple: you provide some callbacks and a list of files. * When an overlaid file gets read, your callbacks get called instead of pulling from the underlying DVD. * - * Using overlay files results in the DVD EntryNums being observed differently from the original disc. + * Original disc EntryNums are not touched by the overlay system, new files are added "at the end." + * The EntryNums for newly-added files are assigned by the game. */ /** @@ -56,6 +57,12 @@ typedef struct AuroraOverlayFile { */ size_t size; + /** + * \brief The observeable EntryNum of this file. + * + * This value is ignored when replacing files already present on the original disc. + * Newly added files *must* specify a value that's above aurora_dvd_base_entry_count(). + */ s32 entryNum; } AuroraOverlayFile; @@ -101,9 +108,8 @@ void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks); /** * \brief Specify a set of overlay files to be used by the DVD layer. * - * Calling this function immediately applies the new files and rebuilds the FST. This is not thread safe and will - * invalidate existing EntryNums gotten from the DVD API. It is best you only call this once on startup, - * before the game's code has started. + * Calling this function immediately applies the new files and rebuilds the FST. This is not thread safe. + * It is best you only call this once on startup, before the game's code has started. * * This function must be called *after* aurora_dvd_overlay_callbacks. * @@ -112,6 +118,11 @@ void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks); */ void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles); +/** + * \brief Gets the amount of FST entries present on the loaded game disc. + * + * This does not take overlay files into account. + */ s32 aurora_dvd_base_entry_count(); #ifdef __cplusplus From f05da27c00e3fda9144b1f9111fa642763fe8621 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 29 May 2026 11:12:15 +0200 Subject: [PATCH 09/11] Remove debug logs --- lib/dolphin/dvd/fst.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/dolphin/dvd/fst.cpp b/lib/dolphin/dvd/fst.cpp index 91845ac740..cd5002f77c 100644 --- a/lib/dolphin/dvd/fst.cpp +++ b/lib/dolphin/dvd/fst.cpp @@ -103,7 +103,6 @@ void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFil return; } - Log.debug("Adding new entry num: {} -> {}", overlayFile.entryNum, overlayFile.fileName); newNode.originalEntryNum = overlayFile.entryNum; node->children.emplace_back(std::make_shared(std::move(newNode))); From eaffd46abba7f6cc5efea02fcc789b16d9d949aa Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 30 May 2026 10:55:36 -0600 Subject: [PATCH 10/11] Aurora-allocated entry nums & dir support --- cmake/aurora_dvd.cmake | 3 +- include/aurora/dvd.h | 14 ++-- lib/dolphin/dvd/dvd.cpp | 95 ++++++++++++++------------ lib/dolphin/dvd/dvd.hpp | 23 +++---- lib/dolphin/dvd/fst.cpp | 146 +++++++++++++++++++++++++++++++--------- 5 files changed, 183 insertions(+), 98 deletions(-) diff --git a/cmake/aurora_dvd.cmake b/cmake/aurora_dvd.cmake index 35e0c81273..c7e8eacf94 100644 --- a/cmake/aurora_dvd.cmake +++ b/cmake/aurora_dvd.cmake @@ -6,5 +6,4 @@ set_target_properties(aurora_dvd PROPERTIES FOLDER "aurora") target_compile_definitions(aurora_dvd PUBLIC AURORA TARGET_PC) target_include_directories(aurora_dvd PUBLIC include) -target_link_libraries(aurora_dvd PUBLIC nod::nod ${AURORA_SDL3_TARGET}) -target_link_libraries(aurora_dvd PRIVATE fmt::fmt) +target_link_libraries(aurora_dvd PUBLIC nod::nod fmt::fmt ${AURORA_SDL3_TARGET}) diff --git a/include/aurora/dvd.h b/include/aurora/dvd.h index 5fdac71e52..e6ae8620ce 100644 --- a/include/aurora/dvd.h +++ b/include/aurora/dvd.h @@ -27,8 +27,8 @@ void aurora_dvd_close(void); * The way this works is pretty simple: you provide some callbacks and a list of files. * When an overlaid file gets read, your callbacks get called instead of pulling from the underlying DVD. * - * Original disc EntryNums are not touched by the overlay system, new files are added "at the end." - * The EntryNums for newly-added files are assigned by the game. + * Original disc EntryNums are not touched by the overlay system. New files and directories + * are assigned stable EntryNums by Aurora. */ /** @@ -57,13 +57,6 @@ typedef struct AuroraOverlayFile { */ size_t size; - /** - * \brief The observeable EntryNum of this file. - * - * This value is ignored when replacing files already present on the original disc. - * Newly added files *must* specify a value that's above aurora_dvd_base_entry_count(). - */ - s32 entryNum; } AuroraOverlayFile; /** @@ -115,8 +108,9 @@ void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks); * * @param files Array of AuroraOverlayFiles, one for every file being overlaid. * @param nFiles Amount of files in the array. + * @param outEntryNums Optional output array receiving one EntryNum per input file. Unaccepted files receive -1. */ -void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles); +void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles, s32* outEntryNums); /** * \brief Gets the amount of FST entries present on the loaded game disc. diff --git a/lib/dolphin/dvd/dvd.cpp b/lib/dolphin/dvd/dvd.cpp index 9747737b19..863ecb854b 100644 --- a/lib/dolphin/dvd/dvd.cpp +++ b/lib/dolphin/dvd/dvd.cpp @@ -24,12 +24,12 @@ using namespace aurora::dvd::impl; namespace aurora::dvd::impl { NodHandle* s_partition = nullptr; std::vector s_fstEntries; - // Map from "virtual" FST entryNums (matching base disc, game-assigned for new overlay files) - // To the "physical" FST entryNums (that we use for navigating the tree). + // Map from public FST entryNums (matching base disc, Aurora-assigned for new overlay entries) + // To the current FST indexes (that we use for navigating the tree). // Unfilled spots are given the k_invalidFstEntry value. - std::vector s_fstEntryMap; - s32 s_baseEntryCount; - PhysicalEntryNum s_currentDir = 0; + std::vector s_entryNumToFstIndex; + s32 s_baseEntryCount = 0; + FstIndex s_currentDir = 0; std::string s_currentPath = "/"; BOOL s_autoInvalidation = FALSE; BOOL s_autoFatalMessaging = FALSE; @@ -96,17 +96,20 @@ void clearState() { s_disc = nullptr; } s_fstEntries.clear(); + s_entryNumToFstIndex.clear(); + s_baseEntryCount = 0; s_currentDir = 0; s_currentPath = "/"; s_diskID = {}; s_initialized = false; } -bool isValidVirtualEntry(VirtualEntryNum entry) { - return entry >= 0 && static_cast(entry) < s_fstEntryMap.size() && s_fstEntryMap[entry] != k_invalidFstEntry; +bool isValidEntryNum(s32 entry) { + return entry >= 0 && static_cast(entry) < s_entryNumToFstIndex.size() && + s_entryNumToFstIndex[entry] != k_invalidFstEntry; } -bool isValidPhysicalEntry(PhysicalEntryNum entry) { +bool isValidFstIndex(FstIndex entry) { return entry >= 0 && static_cast(entry) < s_fstEntries.size(); } @@ -159,8 +162,8 @@ bool nameEqualsIgnoreCase(const std::string& lhs, const char* rhs, size_t rhsLen return aurora::dvd::impl::nameEqualsIgnoreCase(lhs, std::string_view(rhs, rhsLen)); } -PhysicalEntryNum findInDir(PhysicalEntryNum dirEntry, const char* name, size_t nameLen) { - if (!isValidPhysicalEntry(dirEntry) || !s_fstEntries[dirEntry].isDir) { +FstIndex findInDir(FstIndex dirEntry, const char* name, size_t nameLen) { + if (!isValidFstIndex(dirEntry) || !s_fstEntries[dirEntry].isDir) { return -1; } @@ -168,7 +171,7 @@ PhysicalEntryNum findInDir(PhysicalEntryNum dirEntry, const char* name, size_t n u32 i = static_cast(dirEntry) + 1; while (i < childEnd && i < s_fstEntries.size()) { if (nameEqualsIgnoreCase(s_fstEntries[i].name, name, nameLen)) { - return static_cast(i); + return static_cast(i); } if (s_fstEntries[i].isDir) { @@ -181,14 +184,14 @@ PhysicalEntryNum findInDir(PhysicalEntryNum dirEntry, const char* name, size_t n return -1; } -std::string buildDirPath(PhysicalEntryNum entryNum) { - if (entryNum <= 0 || !isValidPhysicalEntry(entryNum)) { +std::string buildDirPath(FstIndex entryNum) { + if (entryNum <= 0 || !isValidFstIndex(entryNum)) { return "/"; } std::vector parts; - PhysicalEntryNum cur = entryNum; - while (cur > 0 && isValidPhysicalEntry(cur)) { + FstIndex cur = entryNum; + while (cur > 0 && isValidFstIndex(cur)) { parts.push_back(s_fstEntries[cur].name); auto parent = s_fstEntries[cur].parent; if (parent == cur) { @@ -672,14 +675,14 @@ int DVDSetAutoFatalMessaging(BOOL enable) { return prev; } -VirtualEntryNum DVDConvertPathToEntrynum(const char* pathPtr) { +s32 DVDConvertPathToEntrynum(const char* pathPtr) { std::lock_guard lock(s_fstLock); if (!s_initialized || pathPtr == nullptr || s_fstEntries.empty()) { return -1; } - PhysicalEntryNum current = 0; + FstIndex current = 0; const char* p = pathPtr; if (*p == '/') { ++p; @@ -695,7 +698,7 @@ VirtualEntryNum DVDConvertPathToEntrynum(const char* pathPtr) { break; } - if (!isValidPhysicalEntry(current) || !s_fstEntries[current].isDir) { + if (!isValidFstIndex(current) || !s_fstEntries[current].isDir) { return -1; } @@ -710,7 +713,7 @@ VirtualEntryNum DVDConvertPathToEntrynum(const char* pathPtr) { } else if (compLen == 2 && p[0] == '.' && p[1] == '.') { current = static_cast(s_fstEntries[current].parent); } else { - const PhysicalEntryNum found = findInDir(current, p, compLen); + const FstIndex found = findInDir(current, p, compLen); if (found < 0) { return -1; } @@ -719,21 +722,21 @@ VirtualEntryNum DVDConvertPathToEntrynum(const char* pathPtr) { p = compEnd; } - assert(isValidPhysicalEntry(current)); + assert(isValidFstIndex(current)); return s_fstEntries[current].origEntryNum; } -BOOL DVDFastOpen(VirtualEntryNum entrynum, DVDFileInfo* fileInfo) { +BOOL DVDFastOpen(s32 entrynum, DVDFileInfo* fileInfo) { std::lock_guard lock(s_fstLock); - if (!s_initialized || fileInfo == nullptr || !isValidVirtualEntry(entrynum) || s_partition == nullptr) { + if (!s_initialized || fileInfo == nullptr || !isValidEntryNum(entrynum) || s_partition == nullptr) { return FALSE; } - const auto physical = s_fstEntryMap[entrynum]; - assert(physical >= 0); + const auto fstIndex = s_entryNumToFstIndex[entrynum]; + assert(fstIndex >= 0); - const auto& entry = s_fstEntries[physical]; + const auto& entry = s_fstEntries[fstIndex]; if (entry.isDir) { return FALSE; } @@ -764,7 +767,7 @@ BOOL DVDFastOpen(VirtualEntryNum entrynum, DVDFileInfo* fileInfo) { } BOOL DVDOpen(const char* fileName, DVDFileInfo* fileInfo) { - VirtualEntryNum entrynum = DVDConvertPathToEntrynum(fileName); + s32 entrynum = DVDConvertPathToEntrynum(fileName); if (entrynum < 0) { return FALSE; } @@ -795,21 +798,21 @@ BOOL DVDGetCurrentDir(char* path, u32 maxlen) { } BOOL DVDChangeDir(const char* dirName) { - VirtualEntryNum entry = DVDConvertPathToEntrynum(dirName); + s32 entry = DVDConvertPathToEntrynum(dirName); std::lock_guard lock(s_fstLock); - if (!isValidVirtualEntry(entry)) { + if (!isValidEntryNum(entry)) { return FALSE; } - const auto physical = s_fstEntryMap[entry]; - if (!s_fstEntries[physical].isDir) { + const auto fstIndex = s_entryNumToFstIndex[entry]; + if (!s_fstEntries[fstIndex].isDir) { return FALSE; } - s_currentDir = physical; - s_currentPath = buildDirPath(physical); + s_currentDir = fstIndex; + s_currentPath = buildDirPath(fstIndex); return TRUE; } @@ -872,26 +875,26 @@ s32 DVDGetFileInfoStatus(const DVDFileInfo* fileInfo) { return fileInfo->cb.state; } -BOOL DVDFastOpenDir(VirtualEntryNum entrynum, DVDDir* dir) { +BOOL DVDFastOpenDir(s32 entrynum, DVDDir* dir) { std::lock_guard lock(s_fstLock); - if (!isValidVirtualEntry(entrynum) || dir == nullptr) { + if (!isValidEntryNum(entrynum) || dir == nullptr) { return FALSE; } - const auto physical = s_fstEntryMap[entrynum]; - if (!s_fstEntries[physical].isDir) { + const auto fstIndex = s_entryNumToFstIndex[entrynum]; + if (!s_fstEntries[fstIndex].isDir) { return FALSE; } - dir->entryNum = static_cast(physical); - dir->location = static_cast(physical) + 1; - dir->next = s_fstEntries[physical].nextOrLength; + dir->entryNum = static_cast(entrynum); + dir->location = static_cast(fstIndex) + 1; + dir->next = s_fstEntries[fstIndex].nextOrLength; return TRUE; } int DVDOpenDir(const char* dirName, DVDDir* dir) { - VirtualEntryNum entrynum = DVDConvertPathToEntrynum(dirName); + s32 entrynum = DVDConvertPathToEntrynum(dirName); if (entrynum < 0) { return FALSE; } @@ -911,7 +914,7 @@ int DVDReadDir(DVDDir* dir, DVDDirEntry* dirent) { const u32 index = dir->location; FSTEntry& entry = s_fstEntries[index]; - dirent->entryNum = index; + dirent->entryNum = static_cast(entry.origEntryNum); dirent->isDir = entry.isDir ? TRUE : FALSE; dirent->name = entry.name.empty() ? nullptr : entry.name.data(); @@ -933,7 +936,15 @@ void DVDRewindDir(DVDDir* dir) { if (dir == nullptr) { return; } - dir->location = dir->entryNum + 1; + + std::lock_guard lock(s_fstLock); + const s32 entryNum = static_cast(dir->entryNum); + if (!isValidEntryNum(entryNum)) { + return; + } + + const auto fstIndex = s_entryNumToFstIndex[entryNum]; + dir->location = static_cast(fstIndex) + 1; } void* DVDGetFSTLocation(void) { diff --git a/lib/dolphin/dvd/dvd.hpp b/lib/dolphin/dvd/dvd.hpp index 435d4c4377..13cb73accf 100644 --- a/lib/dolphin/dvd/dvd.hpp +++ b/lib/dolphin/dvd/dvd.hpp @@ -16,33 +16,32 @@ namespace aurora::dvd::impl { inline Module Log("aurora::dvd"); -using PhysicalEntryNum = s32; -using VirtualEntryNum = s32; +using FstIndex = s32; constexpr s32 k_invalidFstEntry = -1; struct FSTEntry { std::string name; bool isDir = false; - PhysicalEntryNum parent = 0; + FstIndex parent = 0; u32 nextOrLength = 0; void* overlayData = nullptr; bool isOverlay = false; - VirtualEntryNum origEntryNum = 0; + s32 origEntryNum = 0; }; struct IterateNode { std::string name; bool isDir; - u32 originalEntryNum; + s32 originalEntryNum; u32 size; void* overlayData; bool isOverlay; std::vector> children; - IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum, void* overlayData) + IterateNode(std::string name, bool isDir, u32 size, s32 originalEntryNum, void* overlayData) : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(overlayData), isOverlay(true) {} - IterateNode(std::string name, bool isDir, u32 size, u32 originalEntryNum) + IterateNode(std::string name, bool isDir, u32 size, s32 originalEntryNum) : name(std::move(name)), isDir(isDir), size(size), originalEntryNum(originalEntryNum), overlayData(nullptr), isOverlay(false) {} }; @@ -53,12 +52,12 @@ struct IterateContext { extern NodHandle* s_partition; extern std::vector s_fstEntries; -// Map from "virtual" FST entryNums (matching base disc, game-assigned for new overlay files) -// To the "physical" FST entryNums (that we use for navigating the tree). +// Map from public FST entryNums (matching base disc, Aurora-assigned for new overlay files) +// To the current FST indexes (that we use for navigating the tree). // Unfilled spots are given the k_invalidFstEntry value. -extern std::vector s_fstEntryMap; +extern std::vector s_entryNumToFstIndex; extern s32 s_baseEntryCount; -extern PhysicalEntryNum s_currentDir; +extern FstIndex s_currentDir; extern std::string s_currentPath; extern BOOL s_autoInvalidation; extern BOOL s_autoFatalMessaging; @@ -72,4 +71,4 @@ extern std::mutex s_fstLock; bool rebuildFST(); bool nameEqualsIgnoreCase(std::string_view lhs, std::string_view rhs); -} \ No newline at end of file +} diff --git a/lib/dolphin/dvd/fst.cpp b/lib/dolphin/dvd/fst.cpp index cd5002f77c..09a214923b 100644 --- a/lib/dolphin/dvd/fst.cpp +++ b/lib/dolphin/dvd/fst.cpp @@ -1,6 +1,8 @@ #include "dvd.hpp" #include +#include +#include using namespace aurora::dvd::impl; @@ -10,10 +12,66 @@ struct OverlayFileEntry { std::string fileName; void* userData; u32 size; - s32 entryNum; + s32 entryNum = k_invalidFstEntry; + size_t sourceIndex = 0; }; std::vector s_overlayFiles; +std::unordered_map s_overlayEntryNums; +s32 s_nextOverlayEntryNum = 0; +s32 s_overlayEntryNumBase = 0; + +std::string normalizeOverlayPath(std::string_view path) { + std::string normalized; + normalized.reserve(path.size()); + bool lastWasSlash = false; + for (char ch : path) { + if (ch == '\\') { + ch = '/'; + } + if (ch == '/') { + if (lastWasSlash) { + continue; + } + lastWasSlash = true; + normalized.push_back('/'); + continue; + } + lastWasSlash = false; + if (ch >= 'A' && ch <= 'Z') { + ch = static_cast(ch - 'A' + 'a'); + } + normalized.push_back(ch); + } + if (normalized.size() > 1 && normalized.back() == '/') { + normalized.pop_back(); + } + return normalized; +} + +void syncOverlayEntryAllocator() { + if (s_overlayEntryNumBase == s_baseEntryCount) { + return; + } + + s_overlayEntryNums.clear(); + s_overlayEntryNumBase = s_baseEntryCount; + s_nextOverlayEntryNum = s_baseEntryCount; +} + +s32 allocateOverlayEntryNum(std::string_view path) { + syncOverlayEntryAllocator(); + + std::string normalized = normalizeOverlayPath(path); + auto it = s_overlayEntryNums.find(normalized); + if (it != s_overlayEntryNums.end()) { + return it->second; + } + + const s32 entryNum = s_nextOverlayEntryNum++; + s_overlayEntryNums.emplace(std::move(normalized), entryNum); + return entryNum; +} u32 fstCallback(u32 index, NodNodeKind kind, const char* name, u32 size, void* userData) { @@ -49,9 +107,10 @@ IterateNode* findNode(const IterateNode& node, const std::string_view name) { return nullptr; } -void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFileEntry& overlayFile) { +void mergeOverlayFileIntoContext(const IterateContext& context, OverlayFileEntry& overlayFile) { IterateNode* node = context.root.get(); std::string_view filePath = overlayFile.fileName; + std::string currentPath; assert(filePath.starts_with('/')); filePath = filePath.substr(1); @@ -63,6 +122,8 @@ void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFil const auto segment = filePath.substr(0, nextDelim); filePath = filePath.substr(nextDelim + 1); + currentPath += '/'; + currentPath.append(segment); const auto existingNode = findNode(*node, segment); if (existingNode) { @@ -73,7 +134,8 @@ void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFil node = existingNode; } else { - const auto newNode = std::make_shared(std::string(segment), true, 0, k_invalidFstEntry); + const s32 entryNum = allocateOverlayEntryNum(currentPath); + const auto newNode = std::make_shared(std::string(segment), true, 0, entryNum); node->children.push_back(newNode); node = newNode.get(); } @@ -81,7 +143,11 @@ void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFil // Remainder of fileName is the actual file name, and node is the directory we're in. - auto newNode = IterateNode(std::string(filePath), false, overlayFile.size, overlayFile.entryNum, overlayFile.userData); + std::string fullFilePath = currentPath; + fullFilePath += '/'; + fullFilePath.append(filePath); + + auto newNode = IterateNode(std::string(filePath), false, overlayFile.size, k_invalidFstEntry, overlayFile.userData); const auto existingNode = findNode(*node, filePath); if (existingNode) { if (existingNode->isDir) { @@ -90,44 +156,38 @@ void mergeOverlayFileIntoContext(const IterateContext& context, const OverlayFil } newNode.originalEntryNum = existingNode->originalEntryNum; + overlayFile.entryNum = newNode.originalEntryNum; // Replace existing disc entry. *existingNode = std::move(newNode); } else { // Add new entry. - if (overlayFile.entryNum < s_baseEntryCount) { - Log.error( - "Overlay file {} has entryNum {} which is already used by the base disc!", - overlayFile.fileName, - overlayFile.entryNum); - return; - } - - newNode.originalEntryNum = overlayFile.entryNum; + newNode.originalEntryNum = allocateOverlayEntryNum(fullFilePath); + overlayFile.entryNum = newNode.originalEntryNum; node->children.emplace_back(std::make_shared(std::move(newNode))); } } void mergeOverlayFilesIntoContext(const IterateContext& context) { - for (const auto& overlayFile : s_overlayFiles) { + for (auto& overlayFile : s_overlayFiles) { mergeOverlayFileIntoContext(context, overlayFile); } } -void makeFstRecursive(IterateNode& node, u32 parent) { +void makeFstRecursive(IterateNode& node, FstIndex parent) { if (node.originalEntryNum != k_invalidFstEntry) { - if (s_fstEntryMap.size() <= node.originalEntryNum) { - s_fstEntryMap.resize(node.originalEntryNum + 1, k_invalidFstEntry); + if (s_entryNumToFstIndex.size() <= node.originalEntryNum) { + s_entryNumToFstIndex.resize(node.originalEntryNum + 1, k_invalidFstEntry); } - auto& map = s_fstEntryMap[node.originalEntryNum]; + auto& map = s_entryNumToFstIndex[node.originalEntryNum]; if (map != k_invalidFstEntry) { - Log.error("File {} with virtual entry num {} already exists in map!", node.name, node.originalEntryNum); + Log.error("File {} with entry num {} already exists in map!", node.name, node.originalEntryNum); return; } - map = static_cast(s_fstEntries.size()); + map = static_cast(s_fstEntries.size()); } if (!node.isDir) { @@ -140,14 +200,14 @@ void makeFstRecursive(IterateNode& node, u32 parent) { std::ranges::sort(node.children, [](const auto& a, const auto& b) { return a->name < b->name; }); - const auto ourIndex = s_fstEntries.size(); + const FstIndex ourIndex = static_cast(s_fstEntries.size()); s_fstEntries.emplace_back(node.name, true, parent, 0, node.overlayData, node.isOverlay, node.originalEntryNum); for (const auto& child : node.children) { makeFstRecursive(*child, ourIndex); } - s_fstEntries[ourIndex].nextOrLength = s_fstEntries.size(); + s_fstEntries[ourIndex].nextOrLength = static_cast(s_fstEntries.size()); } void makeFstFromContext(const IterateContext& context) { @@ -177,11 +237,6 @@ bool validateOverlayFile(const AuroraOverlayFile& file) { return false; } - if (file.entryNum < 0) { - Log.error("Overlay file entry is below zero: {}", name); - return false; - } - return true; } @@ -198,19 +253,35 @@ bool rebuildFST() { std::lock_guard lock(s_fstLock); - // TODO: Ensure current dir still valid after rebuild. + s32 currentDirEntryNum = k_invalidFstEntry; + const std::string currentPath = s_currentPath; + if (s_currentDir >= 0 && static_cast(s_currentDir) < s_fstEntries.size() && s_fstEntries[s_currentDir].isDir) { + currentDirEntryNum = s_fstEntries[s_currentDir].origEntryNum; + } s_fstEntries.clear(); - s_fstEntryMap.clear(); + s_entryNumToFstIndex.clear(); IterateContext ctx; - ctx.root = std::make_shared(""s, true, 0, static_cast(0)); + ctx.root = std::make_shared(""s, true, 0, 0); ctx.dirStack.emplace_back(ctx.root, std::numeric_limits::max()); nod_partition_iterate_fst(s_partition, fstCallback, &ctx); s_baseEntryCount = calcEntryCount(*ctx.root); + syncOverlayEntryAllocator(); mergeOverlayFilesIntoContext(ctx); makeFstFromContext(ctx); + if (currentDirEntryNum >= 0 && static_cast(currentDirEntryNum) < s_entryNumToFstIndex.size()) { + const FstIndex currentDir = s_entryNumToFstIndex[currentDirEntryNum]; + if (currentDir >= 0 && static_cast(currentDir) < s_fstEntries.size() && s_fstEntries[currentDir].isDir) { + s_currentDir = currentDir; + s_currentPath = currentPath; + return true; + } + } + + s_currentDir = 0; + s_currentPath = "/"; return true; } @@ -240,12 +311,15 @@ s32 aurora_dvd_base_entry_count() { return s_baseEntryCount; } -void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles) { +void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles, s32* outEntryNums) { if (!s_overlayCallbacksSet) { Log.fatal("aurora_dvd_overlay_callbacks not called before aurora_dvd_overlay_files!"); } s_overlayFiles.clear(); + if (outEntryNums != nullptr) { + std::fill_n(outEntryNums, nFiles, k_invalidFstEntry); + } for (size_t i = 0; i < nFiles; i++) { const auto& file = files[i]; @@ -254,10 +328,18 @@ void aurora_dvd_overlay_files(const AuroraOverlayFile* files, size_t nFiles) { continue; } - s_overlayFiles.emplace_back(file.fileName, file.userData, static_cast(file.size), file.entryNum); + s_overlayFiles.emplace_back(file.fileName, file.userData, static_cast(file.size), k_invalidFstEntry, i); } rebuildFST(); + + if (outEntryNums != nullptr) { + for (const auto& file : s_overlayFiles) { + if (file.entryNum != k_invalidFstEntry) { + outEntryNums[file.sourceIndex] = file.entryNum; + } + } + } } void aurora_dvd_overlay_callbacks(const AuroraOverlayCallbacks* callbacks) { From 4f28b81c7daf1d2c1fe25a4c8b08b95985a4c9de Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 30 May 2026 13:03:30 -0600 Subject: [PATCH 11/11] Add warning for resetting cwd --- lib/dolphin/dvd/fst.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/dolphin/dvd/fst.cpp b/lib/dolphin/dvd/fst.cpp index 09a214923b..c416f8a1dc 100644 --- a/lib/dolphin/dvd/fst.cpp +++ b/lib/dolphin/dvd/fst.cpp @@ -280,6 +280,11 @@ bool rebuildFST() { } } + if (currentDirEntryNum != k_invalidFstEntry) { + Log.warn("Current DVD directory {} with entryNum {} was lost during FST rebuild; resetting to root", + currentPath, currentDirEntryNum); + } + s_currentDir = 0; s_currentPath = "/"; return true;