Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/port/GameVersion/BaseGameVersion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,23 @@ 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;
if (sResolved) {
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
}

Expand Down
3 changes: 3 additions & 0 deletions src/port/GameVersion/BaseGameVersion.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
61 changes: 61 additions & 0 deletions src/port/Localization/Language.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <vector>

#include <spdlog/spdlog.h>
#include <zip.h>

#include "libultraship/libultraship.h"
#include "ship/Context.h"
Expand Down Expand Up @@ -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<uint32_t>(st.crc);
outSize = static_cast<uint64_t>(st.size);
return true;
}

size_t DropUnchangedOverrides(const std::string& packArchivePath,
std::unordered_map<uint32_t, std::string>& 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) {
Expand Down Expand Up @@ -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
Expand Down
74 changes: 53 additions & 21 deletions src/port/Resource/Alt/AltDialogFont.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <cstdint>
#include <cstring>
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>

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<std::string> sBuiltSheets;
std::string fontSheetPath(std::string resolved) {
return resolved.empty() ? kFontBaseFallback : resolved;
}

std::shared_ptr<Fast::Texture> loadTex(const std::string& path, bool loadExact) {
return std::static_pointer_cast<Fast::Texture>(
Ship::Context::GetRawInstance()->GetResourceManager()->LoadResourceProcess(path, loadExact));
}

// Build a corrected alt for one chunk and cache it under "alt/<basePath>". 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<Fast::Texture> 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/<activePath>". 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) {
Expand All @@ -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.
Expand All @@ -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<Ship::ResourceInitData>();
initData->Path = altPath;
auto tex = std::make_shared<Fast::Texture>(initData);
Expand All @@ -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);
}
}
20 changes: 12 additions & 8 deletions src/port/ResourceHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,19 +222,23 @@ 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;
}
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) {
Expand All @@ -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(), '\\', '/');

Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/port/ResourceHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Mtx* ResourceMgr_LoadMtxByName(char* path);

void ResourceHelpers_ApplyLanguage(std::unordered_map<uint32_t, std::string> dialogOverride, bool isJapanese,
int dialogCount, int dialogIndex);
std::string ResourceHelpers_GetBaseAssetPath(uint32_t assetId);
std::string ResourceHelpers_GetActiveAssetPath(uint32_t assetId);
#endif

#endif
Loading