diff --git a/src/port/GameVersion/BaseGameVersion.cpp b/src/port/GameVersion/BaseGameVersion.cpp index 717bbb0fd..a38b55c0c 100644 --- a/src/port/GameVersion/BaseGameVersion.cpp +++ b/src/port/GameVersion/BaseGameVersion.cpp @@ -54,6 +54,14 @@ bool ReadStampedCrc(const std::string& archivePath, uint32_t& outCrc) { } // namespace +std::string BaseArchivePath() { + std::string basePath = Ship::Context::LocateFileAcrossAppDirs("bk.o2r", "bk"); + if (basePath.empty() || !std::filesystem::exists(basePath)) { + return std::string(); // not extracted yet + } + return basePath; +} + BKVersion GetBaseVersion() { static BKVersion sVersion = BK_VER_US_10; static bool sResolved = false; @@ -61,8 +69,8 @@ BKVersion GetBaseVersion() { return sVersion; } - std::string basePath = Ship::Context::LocateFileAcrossAppDirs("bk.o2r", "bk"); - if (basePath.empty() || !std::filesystem::exists(basePath)) { + const std::string basePath = BaseArchivePath(); + if (basePath.empty()) { return sVersion; // not extracted yet — retry on the next call } diff --git a/src/port/GameVersion/BaseGameVersion.h b/src/port/GameVersion/BaseGameVersion.h index 916293690..a6c9ae2f0 100644 --- a/src/port/GameVersion/BaseGameVersion.h +++ b/src/port/GameVersion/BaseGameVersion.h @@ -12,6 +12,9 @@ BKVersion GetBaseVersion(); // Classify a version stamp taken from an archive bool ClassifyArchiveVersion(uint32_t rawVersion, BKVersion& out); +// Filesystem path of the base bk.o2r, or empty if it hasn't been extracted yet +std::string BaseArchivePath(); + // Romhacks are generally only supported by US 1.0 bool BaseGameSupportsRomhacks(); diff --git a/src/port/Localization/Language.cpp b/src/port/Localization/Language.cpp index c066c854f..85c2a3ef6 100644 --- a/src/port/Localization/Language.cpp +++ b/src/port/Localization/Language.cpp @@ -8,6 +8,7 @@ #include #include +#include #include "libultraship/libultraship.h" #include "ship/Context.h" @@ -138,6 +139,61 @@ bool ArchiveHasAssetHex(Ship::Archive* archive, uint32_t hex) { return false; } +bool IsLocalizedText(const std::string& path) { + return path.find("/dialog/") != std::string::npos || path.find("/quizq/") != std::string::npos || + path.find("/gruntyq/") != std::string::npos; +} + +bool EntryStamp(zip_t* z, const std::string& entryPath, uint32_t& outCrc, uint64_t& outSize) { + zip_stat_t st; + zip_stat_init(&st); + if (zip_stat(z, entryPath.c_str(), 0, &st) != 0 || !(st.valid & ZIP_STAT_CRC) || !(st.valid & ZIP_STAT_SIZE)) { + return false; + } + outCrc = static_cast(st.crc); + outSize = static_cast(st.size); + return true; +} + +size_t DropUnchangedOverrides(const std::string& packArchivePath, + std::unordered_map& overrides) { + const std::string basePath = BaseArchivePath(); + if (basePath.empty() || packArchivePath.empty()) { + return 0; + } + zip_t* packZip = zip_open(packArchivePath.c_str(), ZIP_RDONLY, nullptr); + if (packZip == nullptr) { + return 0; // a folder-based archive, not a zip: no cheap stamp to compare + } + zip_t* baseZip = zip_open(basePath.c_str(), ZIP_RDONLY, nullptr); + if (baseZip == nullptr) { + zip_close(packZip); + return 0; + } + + size_t dropped = 0; + for (auto it = overrides.begin(); it != overrides.end();) { + if (IsLocalizedText(it->second)) { + ++it; + continue; + } + uint32_t packCrc = 0, baseCrc = 0; + uint64_t packSize = 0, baseSize = 0; + const std::string baseEntry = ResourceHelpers_GetBaseAssetPath(it->first); + if (baseEntry.empty() || !EntryStamp(packZip, it->second, packCrc, packSize) || + !EntryStamp(baseZip, baseEntry, baseCrc, baseSize) || packCrc != baseCrc || packSize != baseSize) { + ++it; + continue; + } + it = overrides.erase(it); + dropped++; + } + + zip_close(baseZip); + zip_close(packZip); + return dropped; +} + const LanguageEntry* FindLanguage(const std::string& name) { for (const auto& e : sLanguages) { if (e.name == name) { @@ -286,6 +342,11 @@ void SetActiveLanguage(const std::string& name) { } dialogOverride[v10Id] = path; } + + if (const size_t dropped = DropUnchangedOverrides(entry->source->GetPath(), dialogOverride); dropped > 0) { + SPDLOG_INFO("[Lang] '{}': {} pack asset(s) are identical to the base game and were left un-repointed", name, + dropped); + } } // Hand the computed language to the resource layer diff --git a/src/port/Resource/Alt/AltDialogFont.cpp b/src/port/Resource/Alt/AltDialogFont.cpp index f3f477398..76c2782c6 100644 --- a/src/port/Resource/Alt/AltDialogFont.cpp +++ b/src/port/Resource/Alt/AltDialogFont.cpp @@ -15,34 +15,56 @@ #include "port/Enhancements/Events/Hooks/Events.h" #include "port/Patches/Patches.h" +#include "port/ResourceHelpers.h" #include "port/ShipInit.hpp" #include #include #include #include +#include #include namespace { -constexpr const char* kFontBase = "assets/sprite/ASSET_6EB_DIALOG_FONT_ALPHAMASK"; - -bool sBuilt = false; +constexpr uint32_t kDialogFontAssetId = 0x6EB; +constexpr const char* kFontBaseFallback = "assets/sprite/ASSET_6EB_DIALOG_FONT_ALPHAMASK"; +std::unordered_set sBuiltSheets; +std::string fontSheetPath(std::string resolved) { + return resolved.empty() ? kFontBaseFallback : resolved; +} std::shared_ptr loadTex(const std::string& path, bool loadExact) { return std::static_pointer_cast( Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path, loadExact)); } -// Build a corrected alt for one chunk and cache it under "alt/". Returns false when there -// is nothing to do: no alt shipped, the dims already reconcile, or the alt isn't an HD raw texture. -bool correctChunk(const std::string& basePath) { - auto base = loadTex(basePath, true); // live native cell +// HD art for one chunk: the active sheet's own alt when the pack ships one, else the base game's +// alt for the same slot. Null when neither has art for it. +std::shared_ptr loadHdChunk(const std::string& activePath, const std::string& basePath, + const Fast::Texture* nativeCell, bool& fromBaseSheet) { + fromBaseSheet = false; + auto alt = loadTex(activePath, false); + if (alt != nullptr && alt.get() != nativeCell) { + return alt; + } + if (basePath == activePath) { + return nullptr; + } + fromBaseSheet = true; + return loadTex(Ship::IResource::gAltAssetPrefix + basePath, true); +} + +// Build a corrected alt for one chunk and cache it under "alt/". Returns false when +// there is nothing to do: no alt shipped, the dims already reconcile, or it isn't an HD raw texture. +bool correctChunk(const std::string& activePath, const std::string& basePath) { + auto base = loadTex(activePath, true); // live native cell if (base == nullptr || base->Width <= 0 || base->Height <= 0) { return false; } - auto alt = loadTex(basePath, false); // HD art, or the base itself if the pack has no alt - if (alt == nullptr || alt->ImageData == nullptr || alt.get() == base.get()) { + bool fromBaseSheet = false; + auto alt = loadHdChunk(activePath, basePath, base.get(), fromBaseSheet); + if (alt == nullptr || alt->ImageData == nullptr) { return false; } if (alt->Type != Fast::TextureType::RGBA32bpp || (alt->Flags & TEX_FLAG_LOAD_AS_RAW) == 0) { @@ -64,8 +86,10 @@ bool correctChunk(const std::string& basePath) { const int dstW = nativeW * k; const int dstH = nativeH * k; - if (dstH == (int)alt->Height) { - return false; // alt height already matches the live native cell -> no drift, nothing to fix + // Matching heights mean no drift to correct. Art borrowed from the base sheet still has to be + // re-cached, though: nothing is reachable at the active path until we put it there. + if (dstH == (int)alt->Height && !fromBaseSheet) { + return false; } // Top-align the HD rows into the k-scaled native cell; the zero-filled remainder stays transparent. @@ -79,7 +103,7 @@ bool correctChunk(const std::string& basePath) { // Same HD pixels, but VPixelScale re-derived against the live native rows (native_rows * k == dstH). // Cache under the alt path so the resource layer returns this in place of the raw archive alt. - const std::string altPath = std::string("alt/") + basePath; + const std::string altPath = Ship::IResource::gAltAssetPrefix + activePath; auto initData = std::make_shared(); initData->Path = altPath; auto tex = std::make_shared(initData); @@ -99,30 +123,38 @@ bool correctChunk(const std::string& basePath) { } // namespace -// Called when the dialog font is (re)loaded. Scans every font chunk once per boot and corrects each; -// idempotent, and a no-op when no alt pack is mounted (a later call may still succeed). +// Called when the dialog font is (re)loaded, boot and language switch alike. Scans every chunk of +// the sheet now in play and corrects each. extern "C" void port_dialogFontHd_rebuild(void) { - if (sBuilt) { + if (!Ship::Context::GetRawInstance()->GetResourceManager()->IsAltAssetsEnabled()) { return; } - if (!Ship::Context::GetRawInstance()->GetResourceManager()->IsAltAssetsEnabled()) { + + const std::string activeFont = fontSheetPath(ResourceHelpers_GetActiveAssetPath(kDialogFontAssetId)); + if (sBuiltSheets.count(activeFont) != 0) { return; } - sBuilt = true; + const std::string baseFont = fontSheetPath(ResourceHelpers_GetBaseAssetPath(kDialogFontAssetId)); + bool built = false; for (int frame = 0; frame < 64; frame++) { bool anyInFrame = false; for (int chunk = 0; chunk < 256; chunk++) { - const std::string basePath = - std::string(kFontBase) + "_" + std::to_string(frame) + "_" + std::to_string(chunk); - if (loadTex(basePath, true) == nullptr) { + const std::string suffix = "_" + std::to_string(frame) + "_" + std::to_string(chunk); + const std::string activePath = activeFont + suffix; + if (loadTex(activePath, true) == nullptr) { break; } anyInFrame = true; - correctChunk(basePath); + built = true; + correctChunk(activePath, baseFont + suffix); } if (!anyInFrame) { break; } } + + if (built) { + sBuiltSheets.insert(activeFont); + } } diff --git a/src/port/ResourceHelpers.cpp b/src/port/ResourceHelpers.cpp index cdc500ff2..6f20d5836 100644 --- a/src/port/ResourceHelpers.cpp +++ b/src/port/ResourceHelpers.cpp @@ -222,12 +222,8 @@ extern "C" int ResourceMgr_IsAssetRepointed(uint32_t assetId) { return sDialogOverride.find(assetId) != sDialogOverride.end() ? 1 : 0; } -// Resolve an asset id to its o2r path: the active language override wins for -// re-pointed dialog ids, otherwise the base manifest. -static std::string ResolveAssetPath(uint32_t assetId) { - if (auto ov = sDialogOverride.find(assetId); ov != sDialogOverride.end()) { - return ov->second; - } +// The base game's own o2r path for an asset id, ignoring any active language re-point. +std::string ResourceHelpers_GetBaseAssetPath(uint32_t assetId) { const auto& symbolMap = GetAssetSymbolMap(); if (auto entry = symbolMap.find(assetId); entry != symbolMap.end()) { return entry->second; @@ -235,6 +231,14 @@ static std::string ResolveAssetPath(uint32_t assetId) { return std::string(); } +// The o2r path an asset id resolves to right now. +std::string ResourceHelpers_GetActiveAssetPath(uint32_t assetId) { + if (auto ov = sDialogOverride.find(assetId); ov != sDialogOverride.end()) { + return ov->second; + } + return ResourceHelpers_GetBaseAssetPath(assetId); +} + static char* LoadAndRetainResource(const std::string& path, uint32_t assetId) { auto res = GetResourceByName(path.c_str()); if (res && res->GetRawPointer() != nullptr) { @@ -253,7 +257,7 @@ extern "C" char* ResourceMgr_ReloadByAssetId(uint32_t assetId) { sResourceRefCache.erase(it); } - std::string mappedPath = ResolveAssetPath(assetId); + std::string mappedPath = ResourceHelpers_GetActiveAssetPath(assetId); if (!mappedPath.empty()) { std::replace(mappedPath.begin(), mappedPath.end(), '\\', '/'); @@ -278,7 +282,7 @@ extern "C" char* ResourceMgr_LoadByAssetId(uint32_t assetId) { sResourceRefCache.erase(it); } - std::string mappedPath = ResolveAssetPath(assetId); + std::string mappedPath = ResourceHelpers_GetActiveAssetPath(assetId); if (mappedPath.empty()) { // A reserved-but-empty ROM slot resolves to nothing by design (the game // tolerates it, as on N64); only a genuinely-missing asset is worth tracing. diff --git a/src/port/ResourceHelpers.h b/src/port/ResourceHelpers.h index bb0381307..87e5156bc 100644 --- a/src/port/ResourceHelpers.h +++ b/src/port/ResourceHelpers.h @@ -37,6 +37,8 @@ Mtx* ResourceMgr_LoadMtxByName(char* path); void ResourceHelpers_ApplyLanguage(std::unordered_map dialogOverride, bool isJapanese, int dialogCount, int dialogIndex); +std::string ResourceHelpers_GetBaseAssetPath(uint32_t assetId); +std::string ResourceHelpers_GetActiveAssetPath(uint32_t assetId); #endif #endif