From bffeca69a9be5cf4adbcade4a867ed5892246eb1 Mon Sep 17 00:00:00 2001 From: Thomas Casson Date: Tue, 24 Feb 2026 21:57:27 +0000 Subject: [PATCH 1/4] Initial Panacea code for BBS asset remastering. --- OpenKh.Research.Panacea/Panacea.cpp | 160 +++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 2 deletions(-) diff --git a/OpenKh.Research.Panacea/Panacea.cpp b/OpenKh.Research.Panacea/Panacea.cpp index 8035c8786..7944e3bca 100644 --- a/OpenKh.Research.Panacea/Panacea.cpp +++ b/OpenKh.Research.Panacea/Panacea.cpp @@ -419,7 +419,7 @@ void GetIMZOffsets(void* addr, int baseoff, std::vector& entries) } } -void GetTM2Offsets(void* addr, int baseoff, std::vector& entries) +void GetTM2Offsets(void* addr, int baseoff, std::vector& entries, int flags = 0x20000000) { if (*(int*)addr == '2MIT') { @@ -429,7 +429,7 @@ void GetTM2Offsets(void* addr, int baseoff, std::vector& entries) while (texcnt-- > 0) { int next = *(int*)off; - entries.push_back(*(short*)(off + 12) + baseoff + 0x20000000); + entries.push_back(*(short*)(off + 12) + baseoff + flags); off += next; baseoff += next; } @@ -722,6 +722,146 @@ void GetBAROffsets(void* addr, int baseoff, std::vector& entries) } } +void GetPMOOffsets(void* addr, int baseoff, std::vector& entries) +{ + if (*(int*)addr == '\0OMP') + { + int texCount = ((short*)addr)[4]; + char* texOffset = (char*)addr + 0xA0; + for (int i = 0; i < texCount; i++) + { + int pTm2 = *(int*)(texOffset + (0x20 * i)); + GetTM2Offsets((char*)addr + pTm2, baseoff + pTm2, entries, 0); + } + } +} + +void GetPMPOffsets(void* addr, int baseoff, std::vector& entries) +{ + if (*(int*)addr == '\0PMP') + { + int texCount = ((short*)addr)[13]; + int texOffset = ((int*)addr)[7]; + char* pTex = ((char*)addr + texOffset); + for (int i = 0; i < texCount; i++) + { + int pTm2 = *(int*)(pTex + (0x20 * i)); + GetTM2Offsets((char*)addr + pTm2, baseoff + pTm2, entries, 0); + } + } +} + +void GetL2DOffsets(void* addr, int baseoff, std::vector& entries) +{ + if (*(int*)addr == '@D2L') + { + int sq2pCount = ((int*)addr)[8]; + int oOffsets = ((int*)addr)[9]; + int* offsets = (int*)((char*)addr + oOffsets); + for (int i = 0; i < sq2pCount; i++) + { + int oSQ2P = offsets[i]; + char* pSQ2P = (char*)offsets + oSQ2P; + int oTM2 = ((int*)pSQ2P)[6]; + char* pTM2 = pSQ2P + oTM2; + GetTM2Offsets(pTM2, baseoff + oOffsets + oSQ2P + oTM2, entries, 0); + } + } +} + +void GetTXAOffsets(void* addr, int baseoff, std::vector& entries) +{ + // TODO: Recheck all the sizes and offsets here + if (*(int*)addr == '\0AXT') + { + // Animations can reuse frames. We only want each frame once. + std::set frames = std::set(); + short tex0Count = ((short*)addr)[3]; + int* pTex0 = (int*)((char*)addr + 0x10); + for (int i = 0; i < tex0Count; i++) + { + short tex1Count = ((short*)pTex0)[24]; + int* pTex1 = (int*)((char*)addr + pTex0[13]); + + for (int j = 0; j < tex1Count; j++) + { + short tex2Count = ((short*)pTex1)[9]; + int* pTex2 = (int*)((char*)addr + pTex1[5]); + + for (int k = 0; k < tex2Count; k++) + { + int pframe = *pTex2; + if (pframe != 0) + frames.emplace(baseoff + pframe); + pTex2 += 3; + } + + pTex1 += 6; + } + + pTex0 += 14; + } + entries.insert(entries.end(), frames.begin(), frames.end()); + } +} + +void GetFEPOffsets(void* addr, int baseoff, std::vector& entries) +{ + if ((*(int*)addr & 0x00FFFFFF) == '\0PEF') + { + std::set frames = std::set(); + unsigned short tex_c = ((unsigned short*)addr)[16]; + int* texbp = (int*)((char*)addr + ((int*)addr)[12]); + for (int i = 0; i < tex_c; i++) + { + char* pTexRes = (char*)addr + texbp[i]; + int texSize = ((int*)pTexRes)[2]; + if (texSize > 0) + { + int oTex = ((int*)pTexRes)[3]; + int* pTex = (int*)((char*)addr + oTex); + char hSize = *((char*)pTex + 0x18); + frames.emplace(baseoff + oTex + hSize); + } + } + entries.insert(entries.end(), frames.begin(), frames.end()); + } + + return; +} + +void GetARCOffsets(void* addr, int baseoff, std::vector& entries) +{ + if (*(int*)addr == '\0CRA') + { + short count = ((short*)addr)[3]; + char* pEntry = (char*)addr + 0x10; + char nameBuf[17] = { 0 }; + for (int i = 0; i < count; i++) + { + std::vector currOffs{}; + int offset = ((int*)pEntry)[1]; + char* pName = pEntry + 0x10; + strncpy_s(nameBuf, 17, pName, 16); + char* ext = PathFindExtensionA(nameBuf); + if (!_stricmp(ext, ".pmo")) + GetPMOOffsets((char*)addr + offset, baseoff + offset, currOffs); + else if (!_stricmp(ext, ".pmp")) + GetPMPOffsets((char*)addr + offset, baseoff + offset, currOffs); + else if (!_stricmp(ext, ".tm2")) + GetTM2Offsets((char*)addr + offset, baseoff + offset, currOffs, 0); + else if (!_stricmp(ext, ".l2d")) + GetL2DOffsets((char*)addr + offset, baseoff + offset, currOffs); + else if (!_stricmp(ext, ".txa")) + GetTXAOffsets((char*)addr + offset, baseoff + offset, currOffs); + else if (!_stricmp(ext, ".fep")) + GetFEPOffsets((char*)addr + offset, baseoff + offset, currOffs); + pEntry += 0x20; + entries.insert(entries.end(), currOffs.begin(), currOffs.end()); + } + } +} + void ScanFolder(const std::wstring& folder, std::vector& files) { std::queue folq; @@ -845,6 +985,22 @@ void ScanRemasteredFolder(const wchar_t* path, void* addr, const wchar_t* remas for (int i = 0; i < entries.size(); ++i) assetoffs.push_back(entries[i].origOffset); break; + case OpenKH::GameId::KingdomHeartsBbs: + if (!_wcsicmp(ext, L".arc")) + GetARCOffsets(addr, 0, assetoffs); + else if (!_wcsicmp(ext, L".pmo")) + GetPMOOffsets(addr, 0, assetoffs); + else if (!_wcsicmp(ext, L".pmp")) + GetPMPOffsets(addr, 0, assetoffs); + else if (!_wcsicmp(ext, L".tm2")) + GetTM2Offsets(addr, 0, assetoffs, 0); + else if (!_wcsicmp(ext, L".l2d")) + GetL2DOffsets(addr, 0, assetoffs); + else if (!_wcsicmp(ext, L".txa")) + GetTXAOffsets(addr, 0, assetoffs); + else if (!_wcsicmp(ext, L".fep")) + GetFEPOffsets(addr, 0, assetoffs); + // TODO: Font support: CLU COD INF MTX default: for (int i = 0; i < entries.size(); ++i) assetoffs.push_back(entries[i].origOffset); From 22722592eed35c00c04a8320effac9f6d729bb2e Mon Sep 17 00:00:00 2001 From: Thomas Casson Date: Fri, 27 Feb 2026 17:52:01 +0000 Subject: [PATCH 2/4] Fix switch fallthrough and add some defensive null checks --- OpenKh.Research.Panacea/Panacea.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/OpenKh.Research.Panacea/Panacea.cpp b/OpenKh.Research.Panacea/Panacea.cpp index 7944e3bca..f9f07a1e6 100644 --- a/OpenKh.Research.Panacea/Panacea.cpp +++ b/OpenKh.Research.Panacea/Panacea.cpp @@ -730,8 +730,9 @@ void GetPMOOffsets(void* addr, int baseoff, std::vector& entries) char* texOffset = (char*)addr + 0xA0; for (int i = 0; i < texCount; i++) { - int pTm2 = *(int*)(texOffset + (0x20 * i)); - GetTM2Offsets((char*)addr + pTm2, baseoff + pTm2, entries, 0); + int oTm2 = *(int*)(texOffset + (0x20 * i)); + if (oTm2 != 0) + GetTM2Offsets((char*)addr + oTm2, baseoff + oTm2, entries, 0); } } } @@ -745,8 +746,9 @@ void GetPMPOffsets(void* addr, int baseoff, std::vector& entries) char* pTex = ((char*)addr + texOffset); for (int i = 0; i < texCount; i++) { - int pTm2 = *(int*)(pTex + (0x20 * i)); - GetTM2Offsets((char*)addr + pTm2, baseoff + pTm2, entries, 0); + int oTm2 = *(int*)(pTex + (0x20 * i)); + if (oTm2 != 0) + GetTM2Offsets((char*)addr + oTm2, baseoff + oTm2, entries, 0); } } } @@ -1001,6 +1003,7 @@ void ScanRemasteredFolder(const wchar_t* path, void* addr, const wchar_t* remas else if (!_wcsicmp(ext, L".fep")) GetFEPOffsets(addr, 0, assetoffs); // TODO: Font support: CLU COD INF MTX + break; default: for (int i = 0; i < entries.size(); ++i) assetoffs.push_back(entries[i].origOffset); From 62c83cd18289bba4913d88fca01af2108412a73a Mon Sep 17 00:00:00 2001 From: Thomas Casson Date: Sat, 13 Jun 2026 17:43:09 +0100 Subject: [PATCH 3/4] Update bbs remaster ordering --- OpenKh.Research.Panacea/Panacea.cpp | 62 ++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/OpenKh.Research.Panacea/Panacea.cpp b/OpenKh.Research.Panacea/Panacea.cpp index f9f07a1e6..090047950 100644 --- a/OpenKh.Research.Panacea/Panacea.cpp +++ b/OpenKh.Research.Panacea/Panacea.cpp @@ -813,19 +813,20 @@ void GetFEPOffsets(void* addr, int baseoff, std::vector& entries) { std::set frames = std::set(); unsigned short tex_c = ((unsigned short*)addr)[16]; - int* texbp = (int*)((char*)addr + ((int*)addr)[12]); + int* pTexResArr = (int*)((char*)addr + ((int*)addr)[12]); for (int i = 0; i < tex_c; i++) { - char* pTexRes = (char*)addr + texbp[i]; + char* pTexRes = (char*)addr + pTexResArr[i]; int texSize = ((int*)pTexRes)[2]; if (texSize > 0) { int oTex = ((int*)pTexRes)[3]; int* pTex = (int*)((char*)addr + oTex); - char hSize = *((char*)pTex + 0x18); - frames.emplace(baseoff + oTex + hSize); + int oPixels = pTex[9]; + frames.emplace(baseoff + oTex + oPixels); } } + entries.insert(entries.end(), frames.begin(), frames.end()); } @@ -839,28 +840,63 @@ void GetARCOffsets(void* addr, int baseoff, std::vector& entries) short count = ((short*)addr)[3]; char* pEntry = (char*)addr + 0x10; char nameBuf[17] = { 0 }; + std::unordered_map> fileMap{}; + std::vector>> fileList{}; for (int i = 0; i < count; i++) { std::vector currOffs{}; + int offset = ((int*)pEntry)[1]; char* pName = pEntry + 0x10; strncpy_s(nameBuf, 17, pName, 16); char* ext = PathFindExtensionA(nameBuf); if (!_stricmp(ext, ".pmo")) + { GetPMOOffsets((char*)addr + offset, baseoff + offset, currOffs); + } else if (!_stricmp(ext, ".pmp")) + { GetPMPOffsets((char*)addr + offset, baseoff + offset, currOffs); + } else if (!_stricmp(ext, ".tm2")) + { GetTM2Offsets((char*)addr + offset, baseoff + offset, currOffs, 0); + } else if (!_stricmp(ext, ".l2d")) + { GetL2DOffsets((char*)addr + offset, baseoff + offset, currOffs); + } else if (!_stricmp(ext, ".txa")) + { GetTXAOffsets((char*)addr + offset, baseoff + offset, currOffs); + } else if (!_stricmp(ext, ".fep")) + { GetFEPOffsets((char*)addr + offset, baseoff + offset, currOffs); + } pEntry += 0x20; - entries.insert(entries.end(), currOffs.begin(), currOffs.end()); + auto name = std::string(nameBuf); + fileMap.emplace(name, currOffs); + fileList.emplace_back(name, currOffs); } + + // TXA files immediatly preceed the file they're attatched to. Everything else shows up in order. + for (auto& pair : fileList) + { + auto path = PathFindExtensionA(pair.first.c_str()); + if (!_stricmp(path, ".txa")) continue; + if (!_stricmp(path, ".pmo") || !_stricmp(path, ".pmp")) + { + std::string name2 = pair.first.substr(0, pair.first.length() - 4) + ".txa"; + auto itr = fileMap.find(name2); + if (itr != fileMap.end()) + { + entries.insert(entries.end(), itr->second.begin(), itr->second.end()); + } + } + entries.insert(entries.end(), pair.second.begin(), pair.second.end()); + } + } } @@ -948,6 +984,13 @@ bool sortRemasteredFiles(const Axa::RemasteredEntry& a, const Axa::RemasteredEnt return false; } +bool sortRemasteredFiles_BBS(const Axa::RemasteredEntry& a, const Axa::RemasteredEntry& b) +{ + std::string na = std::string(a.name); + std::string nb = std::string(b.name); + return na < nb; +} + void ScanRemasteredFolder(const wchar_t* path, void* addr, const wchar_t* remasteredFolder, std::vector& entries) { std::vector assetoffs{}; @@ -1002,7 +1045,11 @@ void ScanRemasteredFolder(const wchar_t* path, void* addr, const wchar_t* remas GetTXAOffsets(addr, 0, assetoffs); else if (!_wcsicmp(ext, L".fep")) GetFEPOffsets(addr, 0, assetoffs); + // TODO: .frr // TODO: Font support: CLU COD INF MTX + else // unknown file type + for (int i = 0; i < entries.size(); ++i) + assetoffs.push_back(entries[i].origOffset); break; default: for (int i = 0; i < entries.size(); ++i) @@ -1013,7 +1060,10 @@ void ScanRemasteredFolder(const wchar_t* path, void* addr, const wchar_t* remas ScanFolder(remasteredFolder, modfiles); if (modfiles.size() >= assetoffs.size()) { - std::stable_sort(modfiles.begin(), modfiles.end(), sortRemasteredFiles); + if (OpenKH::m_GameID == OpenKH::GameId::KingdomHeartsBbs) + std::stable_sort(modfiles.begin(), modfiles.end(), sortRemasteredFiles_BBS); + else + std::stable_sort(modfiles.begin(), modfiles.end(), sortRemasteredFiles); entries = modfiles; entries.resize(assetoffs.size()); for (int i = 0; i < entries.size(); ++i) From a0fdd9a459463a11e2aff00682d1361cd78a04cf Mon Sep 17 00:00:00 2001 From: Thomas Casson Date: Sun, 14 Jun 2026 15:39:19 +0100 Subject: [PATCH 4/4] Fix bbs asset ordering, but for real this time. --- OpenKh.Research.Panacea/Panacea.cpp | 34 +++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/OpenKh.Research.Panacea/Panacea.cpp b/OpenKh.Research.Panacea/Panacea.cpp index 090047950..57b2effda 100644 --- a/OpenKh.Research.Panacea/Panacea.cpp +++ b/OpenKh.Research.Panacea/Panacea.cpp @@ -984,15 +984,42 @@ bool sortRemasteredFiles(const Axa::RemasteredEntry& a, const Axa::RemasteredEnt return false; } -bool sortRemasteredFiles_BBS(const Axa::RemasteredEntry& a, const Axa::RemasteredEntry& b) +bool sortRemasteredFiles_LexBBS(const Axa::RemasteredEntry& a, const Axa::RemasteredEntry& b) { std::string na = std::string(a.name); std::string nb = std::string(b.name); return na < nb; } +bool sortRemasteredFiles_NumBBS(const Axa::RemasteredEntry& a, const Axa::RemasteredEntry& b) +{ + // this doesn't work, we need to start at the end and walk back until we find a digit (.dds), then until we find not a digit. + /*const char* pa = a.name; + while (*pa != '\0' && !isdigit(*pa)) pa++; + const char* pb = b.name; + while (*pb != '\0' && !isdigit(*pb)) pb++; + if (*pa == '\0' || *pb == '\0') return false; + int ia = atoi(pa); + int ib = atoi(pb); + return ia < ib;*/ + + char* pa = PathFindExtensionA(a.name) - 1; + while (isdigit(*(pa - 1)) && pa > a.name) pa--; + + char* pb = PathFindExtensionA(b.name) - 1; + while (isdigit(*(pb - 1)) && pb > b.name) pb--; + + int ia = atoi(pa); + int ib = atoi(pb); + return ia < ib; +} + void ScanRemasteredFolder(const wchar_t* path, void* addr, const wchar_t* remasteredFolder, std::vector& entries) { + // BBS wants it's files as 0, 1, 10, 11, 12, 2, 3..., so we need to sort them numerically to work with them then sort them lexically to send to the game. + if (OpenKH::m_GameID == OpenKH::GameId::KingdomHeartsBbs) + std::stable_sort(entries.begin(), entries.end(), sortRemasteredFiles_NumBBS); + std::vector assetoffs{}; const wchar_t* ext = PathFindExtensionW(path); switch (OpenKH::m_GameID) @@ -1061,7 +1088,7 @@ void ScanRemasteredFolder(const wchar_t* path, void* addr, const wchar_t* remas if (modfiles.size() >= assetoffs.size()) { if (OpenKH::m_GameID == OpenKH::GameId::KingdomHeartsBbs) - std::stable_sort(modfiles.begin(), modfiles.end(), sortRemasteredFiles_BBS); + std::stable_sort(modfiles.begin(), modfiles.end(), sortRemasteredFiles_NumBBS); else std::stable_sort(modfiles.begin(), modfiles.end(), sortRemasteredFiles); entries = modfiles; @@ -1092,6 +1119,9 @@ void ScanRemasteredFolder(const wchar_t* path, void* addr, const wchar_t* remas } } } + + if (OpenKH::m_GameID == OpenKH::GameId::KingdomHeartsBbs) + std::stable_sort(entries.begin(), entries.end(), sortRemasteredFiles_LexBBS); } void GetRemasteredFiles(Axa::PackageFile* fileinfo, const wchar_t* path, void* addr)