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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 18:22:03 -06:00
parent f70433c430
commit 3072715dc3
2 changed files with 84 additions and 17 deletions

View File

@@ -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<char*>(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<std::string> 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<std::filesystem::path> 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<char*>(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<std::filesystem::path> 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(), '\\', '/');

View File

@@ -85,6 +85,29 @@ public:
return m_AssetBundleType;
}
/**
* A folder of edited zone files (zone_override_dir): a file under maps/ is read from <dir>/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<std::filesystem::path> 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<std::string> 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;