diff --git a/WeaveLoaderRuntime/src/GameHooks.cpp b/WeaveLoaderRuntime/src/GameHooks.cpp index 0be6abe..330a23a 100644 --- a/WeaveLoaderRuntime/src/GameHooks.cpp +++ b/WeaveLoaderRuntime/src/GameHooks.cpp @@ -341,6 +341,35 @@ namespace GameHooks return path.compare(pathLen - suffLen, suffLen, suffix) == 0; } + static bool TryParseMipmapLevel(const std::wstring& lowerPath, const wchar_t* stem, int& outLevel) + { + outLevel = 0; + if (!stem) return false; + std::wstring key = std::wstring(stem) + L"mipmaplevel"; + size_t pos = lowerPath.rfind(key); + if (pos == std::wstring::npos) + return false; + + size_t numStart = pos + key.size(); + size_t numEnd = lowerPath.find(L".png", numStart); + if (numEnd == std::wstring::npos || numEnd <= numStart) + return false; + + int value = 0; + for (size_t i = numStart; i < numEnd; i++) + { + wchar_t ch = lowerPath[i]; + if (ch < L'0' || ch > L'9') + return false; + value = value * 10 + (ch - L'0'); + } + + if (value <= 1) + return false; + outLevel = value; + return true; + } + static std::string ToLowerAscii(const std::string& value) { std::string out; @@ -2408,6 +2437,26 @@ namespace GameHooks if (!isGenerated) { + int mipLevel = 0; + if (TryParseMipmapLevel(lower, L"terrain", mipLevel)) + { + std::string mipPath = ModAtlas::GetMergedMipmapPath(0, mipLevel); + if (!mipPath.empty()) + { + std::wstring ourPath(mipPath.begin(), mipPath.end()); + return Original_GetResourceAsStream(&ourPath); + } + } + if (TryParseMipmapLevel(lower, L"items", mipLevel)) + { + std::string mipPath = ModAtlas::GetMergedMipmapPath(1, mipLevel); + if (!mipPath.empty()) + { + std::wstring ourPath(mipPath.begin(), mipPath.end()); + return Original_GetResourceAsStream(&ourPath); + } + } + if (EndsWithPath(lower, L"terrain.png")) { ModAtlas::SetOverrideAtlasPath(0, std::string(path->begin(), path->end())); diff --git a/WeaveLoaderRuntime/src/ModAtlas.cpp b/WeaveLoaderRuntime/src/ModAtlas.cpp index 3591c75..e7c2785 100644 --- a/WeaveLoaderRuntime/src/ModAtlas.cpp +++ b/WeaveLoaderRuntime/src/ModAtlas.cpp @@ -240,6 +240,111 @@ namespace ModAtlas return ok != 0 && !outBytes.empty(); } + struct MipLevel + { + int w = 0; + int h = 0; + std::vector rgba; + }; + + static void Downsample2x(const unsigned char* src, int srcW, int srcH, + std::vector& out, int& outW, int& outH) + { + outW = (srcW > 1) ? (srcW / 2) : 1; + outH = (srcH > 1) ? (srcH / 2) : 1; + out.assign(static_cast(outW) * static_cast(outH) * 4, 0); + + for (int y = 0; y < outH; y++) + { + int sy = y * 2; + for (int x = 0; x < outW; x++) + { + int sx = x * 2; + int sx1 = (sx + 1 < srcW) ? (sx + 1) : (srcW - 1); + int sy1 = (sy + 1 < srcH) ? (sy + 1) : (srcH - 1); + + const unsigned char* p00 = src + (sy * srcW + sx) * 4; + const unsigned char* p10 = src + (sy * srcW + sx1) * 4; + const unsigned char* p01 = src + (sy1 * srcW + sx) * 4; + const unsigned char* p11 = src + (sy1 * srcW + sx1) * 4; + + unsigned int r = p00[0] + p10[0] + p01[0] + p11[0]; + unsigned int g = p00[1] + p10[1] + p01[1] + p11[1]; + unsigned int b = p00[2] + p10[2] + p01[2] + p11[2]; + unsigned int a = p00[3] + p10[3] + p01[3] + p11[3]; + + unsigned char* dst = out.data() + (y * outW + x) * 4; + dst[0] = static_cast(r / 4); + dst[1] = static_cast(g / 4); + dst[2] = static_cast(b / 4); + dst[3] = static_cast(a / 4); + } + } + } + + static void BuildMipChain(const unsigned char* base, int baseW, int baseH, + std::vector& outLevels) + { + outLevels.clear(); + if (!base || baseW <= 0 || baseH <= 0) + return; + + std::vector current(base, base + (static_cast(baseW) * baseH * 4)); + int curW = baseW; + int curH = baseH; + + for (int level = 1; level < 10; level++) + { + if (curW == 1 && curH == 1) + break; + + MipLevel mip; + Downsample2x(current.data(), curW, curH, mip.rgba, mip.w, mip.h); + outLevels.push_back(mip); + current = mip.rgba; + curW = mip.w; + curH = mip.h; + } + } + + static void ApplyMipChainToBufferedImage(int** data, const std::vector& levels) + { + if (!data) + return; + + size_t count = (levels.size() > 9) ? 9 : levels.size(); + for (size_t i = 0; i < count; i++) + { + const MipLevel& mip = levels[i]; + if (mip.w <= 0 || mip.h <= 0 || mip.rgba.empty()) + continue; + + int pixelCount = mip.w * mip.h; + int* buf = new int[pixelCount]; + for (int p = 0; p < pixelCount; p++) + { + const unsigned char* px = &mip.rgba[p * 4]; + buf[p] = (static_cast(px[3]) << 24) | + (static_cast(px[0]) << 16) | + (static_cast(px[1]) << 8) | + (static_cast(px[2]) << 0); + } + + if (data[i + 1]) + delete[] data[i + 1]; + data[i + 1] = buf; + } + + for (size_t i = count + 1; i < 10; i++) + { + if (data[i]) + { + delete[] data[i]; + data[i] = nullptr; + } + } + } + static bool FileExists(const std::string& path) { DWORD attr = GetFileAttributesA(path.c_str()); @@ -252,6 +357,12 @@ namespace ModAtlas return baseDir + "\\" + stem + "_p" + std::to_string(page) + ".png"; } + static std::string BuildMipOutputPath(const std::string& baseDir, const char* stem, int level) + { + if (level <= 1) return baseDir + "\\" + stem + ".png"; + return baseDir + "\\" + stem + "MipMapLevel" + std::to_string(level) + ".png"; + } + static std::string BuildVirtualPageOutputPath(const std::string& baseDir, const char* stem, int page) { if (baseDir.empty()) return ""; @@ -522,6 +633,47 @@ namespace ModAtlas return consumed; } + static void WriteMipChainToDisk(const std::string& baseDir, const char* stem, + const std::vector& levels) + { + for (size_t i = 0; i < levels.size() && i < 9; i++) + { + const MipLevel& mip = levels[i]; + if (mip.w <= 0 || mip.h <= 0 || mip.rgba.empty()) + continue; + + std::vector pngBytes; + if (!SavePngToBytes(mip.rgba.data(), mip.w, mip.h, pngBytes)) + continue; + + int mipLevel = static_cast(i) + 2; + std::string outPath = BuildMipOutputPath(baseDir, stem, mipLevel); + std::ofstream out(outPath, std::ios::binary); + if (out.is_open()) + out.write(reinterpret_cast(pngBytes.data()), + static_cast(pngBytes.size())); + } + } + + static void GenerateMipmapsForAtlas(const std::string& basePath, + const std::string& outDir, + const char* stem) + { + if (basePath.empty() || outDir.empty()) + return; + + int w = 0, h = 0, c = 0; + unsigned char* base = stbi_load(basePath.c_str(), &w, &h, &c, 4); + if (!base) + return; + + std::vector levels; + BuildMipChain(base, w, h, levels); + stbi_image_free(base); + + WriteMipChainToDisk(outDir, stem, levels); + } + static std::string BuildAtlasesInternal(const std::string& modsPath, const std::string& terrainBasePath, const std::string& itemsBasePath) @@ -597,6 +749,8 @@ namespace ModAtlas if (!vpath.empty()) CopyFileA(outPath.c_str(), vpath.c_str(), FALSE); } + + GenerateMipmapsForAtlas(outPath, s_mergedDir, "terrain"); } if (placed < blockPaths.size()) @@ -899,6 +1053,13 @@ namespace ModAtlas *reinterpret_cast(base + 0x50) = outW; *reinterpret_cast(base + 0x54) = outH; } + + if (atlasType == 0) + { + std::vector levels; + BuildMipChain(mergedRgba.data(), outW, outH, levels); + ApplyMipChainToBufferedImage(data, levels); + } return true; } @@ -1030,6 +1191,17 @@ namespace ModAtlas return BuildPageOutputPath(s_mergedDir, "items", page); } + std::string GetMergedMipmapPath(int atlasType, int mipLevel) + { + if (s_mergedDir.empty() || mipLevel <= 1) + return ""; + const char* stem = (atlasType == 0) ? "terrain" : "items"; + std::string path = BuildMipOutputPath(s_mergedDir, stem, mipLevel); + if (!FileExists(path)) + return ""; + return path; + } + std::string GetVirtualPagePath(int atlasType, int page) { if (s_virtualAtlasDir.empty() || page < 0) return ""; @@ -1110,6 +1282,44 @@ namespace ModAtlas (lower.find(L"/modloader/") != std::wstring::npos); } + static bool TryExtractMipmapLevel(const wchar_t* path, const wchar_t* stem, int& outLevel) + { + outLevel = 0; + if (!path || !stem) return false; + + std::wstring lower; + lower.reserve(wcslen(path)); + for (const wchar_t* p = path; *p; ++p) + { + wchar_t c = (*p == L'\\') ? L'/' : *p; + lower.push_back((wchar_t)towlower(c)); + } + + std::wstring key = std::wstring(stem) + L"mipmaplevel"; + size_t pos = lower.rfind(key); + if (pos == std::wstring::npos) + return false; + + size_t numStart = pos + key.size(); + size_t numEnd = lower.find(L".png", numStart); + if (numEnd == std::wstring::npos || numEnd <= numStart) + return false; + + int value = 0; + for (size_t i = numStart; i < numEnd; i++) + { + wchar_t ch = lower[i]; + if (ch < L'0' || ch > L'9') + return false; + value = value * 10 + (ch - L'0'); + } + + if (value <= 1) + return false; + outLevel = value; + return true; + } + static HANDLE WINAPI Hooked_CreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, @@ -1122,6 +1332,27 @@ namespace ModAtlas } if (lpFileName && s_hasModTextures && !IsGeneratedAtlasPath(lpFileName)) { + int mipLevel = 0; + if (TryExtractMipmapLevel(lpFileName, L"terrain", mipLevel)) + { + std::string mipPath = GetMergedMipmapPath(0, mipLevel); + if (!mipPath.empty()) + { + std::wstring wmip(mipPath.begin(), mipPath.end()); + return s_originalCreateFileW(wmip.c_str(), dwDesiredAccess, dwShareMode, + lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + } + } + if (TryExtractMipmapLevel(lpFileName, L"items", mipLevel)) + { + std::string mipPath = GetMergedMipmapPath(1, mipLevel); + if (!mipPath.empty()) + { + std::wstring wmip(mipPath.begin(), mipPath.end()); + return s_originalCreateFileW(wmip.c_str(), dwDesiredAccess, dwShareMode, + lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + } + } if (!s_mergedTerrainW.empty() && EndsWith(lpFileName, L"\\terrain.png")) { LogUtil::Log("[WeaveLoader] CreateFileW: redirecting terrain.png to merged atlas"); diff --git a/WeaveLoaderRuntime/src/ModAtlas.h b/WeaveLoaderRuntime/src/ModAtlas.h index 24f3edd..53088fc 100644 --- a/WeaveLoaderRuntime/src/ModAtlas.h +++ b/WeaveLoaderRuntime/src/ModAtlas.h @@ -47,6 +47,7 @@ namespace ModAtlas std::string GetMergedItemsPath(); std::string GetMergedPagePath(int atlasType, int page); std::string GetVirtualPagePath(int atlasType, int page); + std::string GetMergedMipmapPath(int atlasType, int mipLevel); std::string GetMergedTerrainPage1Path(); std::string GetMergedItemsPage1Path();