From 3072715dc33ffc16264b2e642d27050a2a540cd1 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 18:22:03 -0600 Subject: [PATCH] feat(assets): an override folder for files under maps/ The asset manager reads a name under maps/ from the override folder when the file is there, and from the client otherwise. Nothing else is looked for in it and the client's files are never written. Off until a folder is set. Co-Authored-By: Claude Fable 5.2 --- dCommon/dClient/AssetManager.cpp | 77 +++++++++++++++++++++++++------- dCommon/dClient/AssetManager.h | 24 ++++++++++ 2 files changed, 84 insertions(+), 17 deletions(-) diff --git a/dCommon/dClient/AssetManager.cpp b/dCommon/dClient/AssetManager.cpp index deb40d940..ca9820c54 100644 --- a/dCommon/dClient/AssetManager.cpp +++ b/dCommon/dClient/AssetManager.cpp @@ -70,7 +70,64 @@ void AssetManager::LoadPackIndex() { m_PackIndex = PackIndex(m_RootPath); } +namespace { + // The whole file, malloc'd as GetFile's callers free it + bool ReadLooseFile(const std::filesystem::path& path, char** data, uint32_t* len) { + FILE* file; +#ifdef _WIN32 + fopen_s(&file, path.string().c_str(), "rb"); +#elif __APPLE__ + // macOS has 64bit file IO by default + file = fopen(path.string().c_str(), "rb"); +#else + file = fopen64(path.string().c_str(), "rb"); +#endif + if (!file) return false; + fseek(file, 0, SEEK_END); + *len = ftell(file); + *data = static_cast(malloc(*len)); + fseek(file, 0, SEEK_SET); + const auto read = fread(*data, sizeof(uint8_t), *len, file); + fclose(file); + if (read != *len) { + free(*data); + *data = nullptr; + return false; + } + return true; + } +} + +void AssetManager::SetOverrideDir(const std::filesystem::path& dir) { + m_OverrideDir = dir; +} + +std::optional AssetManager::OverrideName(std::string name) { + std::transform(name.begin(), name.end(), name.begin(), [](uint8_t c) { return std::tolower(c); }); + std::replace(name.begin(), name.end(), '\\', '/'); + if (!name.starts_with("maps/") || name.find('\0') != std::string::npos) return std::nullopt; + size_t start = 0; + while (start <= name.size()) { + const auto end = std::min(name.find('/', start), name.size()); + const auto part = std::string_view(name).substr(start, end - start); + if (part.empty() || part == "." || part == "..") return std::nullopt; + start = end + 1; + } + return name; +} + +std::optional AssetManager::GetOverridePath(std::string name) const { + if (m_OverrideDir.empty()) return std::nullopt; + const auto relative = OverrideName(std::move(name)); + if (!relative) return std::nullopt; + const auto path = m_OverrideDir / *relative; + std::error_code code; + if (!std::filesystem::is_regular_file(path, code)) return std::nullopt; + return path; +} + bool AssetManager::HasFile(std::string fixedName) const { + if (GetOverridePath(fixedName)) return true; std::transform(fixedName.begin(), fixedName.end(), fixedName.begin(), [](uint8_t c) { return std::tolower(c); }); // Special case for unpacked client have BrickModels in upper case @@ -96,6 +153,7 @@ bool AssetManager::HasFile(std::string fixedName) const { } bool AssetManager::GetFile(std::string fixedName, char** data, uint32_t* len) const { + if (const auto overridden = GetOverridePath(fixedName)) return ReadLooseFile(*overridden, data, len); std::transform(fixedName.begin(), fixedName.end(), fixedName.begin(), [](uint8_t c) { return std::tolower(c); }); std::replace(fixedName.begin(), fixedName.end(), '\\', '/'); // On the off chance someone has the wrong slashes, force forward slashes @@ -103,23 +161,7 @@ bool AssetManager::GetFile(std::string fixedName, char** data, uint32_t* len) co if (this->m_AssetBundleType == eAssetBundleType::Unpacked) GeneralUtils::ReplaceInString(fixedName, "brickmodels", "BrickModels"); if (std::filesystem::exists(m_ResPath / fixedName)) { - FILE* file; -#ifdef _WIN32 - fopen_s(&file, (m_ResPath / fixedName).string().c_str(), "rb"); -#elif __APPLE__ - // macOS has 64bit file IO by default - file = fopen((m_ResPath / fixedName).string().c_str(), "rb"); -#else - file = fopen64((m_ResPath / fixedName).string().c_str(), "rb"); -#endif - fseek(file, 0, SEEK_END); - *len = ftell(file); - *data = static_cast(malloc(*len)); - fseek(file, 0, SEEK_SET); - int32_t readInData = fread(*data, sizeof(uint8_t), *len, file); - fclose(file); - - return true; + return ReadLooseFile(m_ResPath / fixedName, data, len); } if (this->m_AssetBundleType == eAssetBundleType::Unpacked) return false; @@ -155,6 +197,7 @@ bool AssetManager::GetFile(std::string fixedName, char** data, uint32_t* len) co } std::optional AssetManager::GetLoosePath(std::string fixedName) const { + if (auto overridden = GetOverridePath(fixedName)) return overridden; // The same name fixing as GetFile std::transform(fixedName.begin(), fixedName.end(), fixedName.begin(), [](uint8_t c) { return std::tolower(c); }); std::replace(fixedName.begin(), fixedName.end(), '\\', '/'); diff --git a/dCommon/dClient/AssetManager.h b/dCommon/dClient/AssetManager.h index 69fe4e8aa..9a7270ce2 100644 --- a/dCommon/dClient/AssetManager.h +++ b/dCommon/dClient/AssetManager.h @@ -85,6 +85,29 @@ public: return m_AssetBundleType; } + /** + * A folder of edited zone files (zone_override_dir): a file under maps/ is read from /maps/... when it is + * there, and from the client otherwise. Nothing else is looked for in it, and the client's files are never + * written. Empty turns it off. + */ + void SetOverrideDir(const std::filesystem::path& dir); + + [[nodiscard]] + const std::filesystem::path& GetOverrideDir() const { + return m_OverrideDir; + } + + // Where name is read from in the override folder: only for a name under maps/ whose file is there + [[nodiscard]] + std::optional GetOverridePath(std::string name) const; + + /** + * The path inside an override folder a name under maps/ is kept at (lowercase, forward slashes), whether or not + * a file is there. nullopt for any other name and for one with an empty, "." or ".." part. + */ + [[nodiscard]] + static std::optional OverrideName(std::string name); + [[nodiscard]] bool HasFile(std::string name) const; @@ -111,6 +134,7 @@ private: std::filesystem::path m_Path; std::filesystem::path m_RootPath; std::filesystem::path m_ResPath; + std::filesystem::path m_OverrideDir; eAssetBundleType m_AssetBundleType = eAssetBundleType::None;