diff --git a/audit/history.jsonl b/audit/history.jsonl index 8a93258d..2189daff 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -490,3 +490,4 @@ {"time":"2026-09-22T16:00:00Z","event":"pack_inventory","scope":"Client/pack (batch 2R)","tool":"tools/epk_scan/epk_scan.py","report":"audit/packs/2R-scan.json","totals":{"registered_packs":136,"entries":54891,"unique_paths":52609,"NONE":20727,"COMPRESS":29812,"SECURITY":4352,"PANAMA":0,"HYBRIDCRYPT":0,"decode_failures":0,"overrides":2282,"overrides_different":2194,"case_collisions":0},"findings":["first-registered pack wins (CEterFileDict multimap, VS2022 STL)","short-layout packer in metin2_patch_{sd,mineral,mundi}: 4 SECURITY files rejected by 40250 CRC check","no server-delivered pack keys needed"]} {"time":"2026-09-23T10:00:00Z","event":"port_round","unit":"EterPack/*, EterBase/{tea,lzo,Timer,Stl,Random}.cpp + platform EterBase/{CRC32,FileBase,MappedFile,Debug,Utils}","ported":["EterPack/EterPack.cpp","EterPack/EterPackManager.cpp","EterPack/EterPackCursor.cpp","EterPack/EterPackPolicy_CSHybridCrypt.cpp","EterBase/tea.cpp","EterBase/lzo.cpp","EterBase/Timer.cpp","EterBase/Stl.cpp","EterBase/Random.cpp"],"status_note":"mirror copies + platform FileBase/MappedFile/CRC32/Debug/Utils; not runtime-reachable until the asset_io pack backend (PORT-PLAN §5 step 5), so port-map statuses stay TODO","divergent":[],"needs_live":[],"corrections":["2R registered_packs 136 was wrong: 124 Index pairs -> 119 RegisterPack successes, 103 distinct packs + root (PORT-PLAN, memory)"],"findings":["libc++ unordered_multimap also keeps first-registered-wins: 0 wrong winners over 2282 overrides","exactly the 4 short-layout SECURITY files fail CEterPackManager::Get","pack/Index spells Etc for ETC.eix: CEterPack::CreateIndexFile fopens directly, so the bootstrap must resolve the on-disk dbname"],"keys":"EterPack TEA keys generated at configure time into the build tree (EterPackKeys.generated.h), never committed","tests":["extension/tests/port_eterpack_test.cpp PASS (macOS, 52605 files / 2023459178 bytes)","script/port_gate.sh macos/android/ios/windows PASS"]} {"time":"2026-09-23T12:00:00Z","event":"port_round","unit":"platform UserInterface PackInitialize + EterBase/FileLoader.cpp + pack:// asset_io backend (PORT-PLAN §5 step 5b)","ported":["EterBase/FileLoader.cpp"],"deleted":[],"divergent":[],"needs_live":[],"status_note":"PackInitialize (platform, on-disk dbname resolution, no LogBoxf) + mtpack40250 backend + Metin2Pack node; no runtime reader uses pack:// until the proto/GDScript switch, so port-map statuses stay TODO","tests":["extension/tests/port_eterpack_test.cpp via PackSingletons+PackInitialize(\"pack\"): 103 packs, oracle order, PASS"],"commit":"5b"} +{"time":"2026-09-23T12:30:00Z","event":"port_round","unit":"proto reader -> 40250 item_proto/mob_proto via CLZO + pack://locale// (PORT-PLAN §5 step 2 + step 5 runtime switch)","ported":["EterPack/EterPack.cpp (16 fns)","EterPack/EterPackManager.cpp (14)","EterPack/EterPackPolicy_CSHybridCrypt.cpp (1)","EterBase/lzo.cpp (11)","EterBase/tea.cpp (2)","EterBase/Stl.cpp (2)"],"adapted":["UserInterface/UserInterface.cpp PackInitialize","EterBase/FileLoader.cpp (5)","EterBase/MappedFile.cpp (9)","EterBase/FileBase.cpp (5)","EterBase/CRC32.cpp GetCRC32"],"deleted":["extension/src/proto/proto.cpp m2dev reader: XChaCha20 + MCOZ, 236-byte item / 335-byte mob strides"],"divergent":[],"needs_live":[],"status_note":"statuses set only for the functions a clang coverage run of proto_test + port_eterpack_test hit; the rest stay TODO. SMobTable is a local #pragma pack(1) mirror in proto.cpp (static_assert 255) until batch 2P ports GameLib/PythonNonPlayer. name = cp1252(szLocaleName) as CItemData::GetName / CPythonNonPlayer::GetMonsterName","tests":["extension/tests/proto_test.cpp: 5743 items / 1334 mobs from pack://locale/en PASS","extension/tests/proto_item_layout_test.cpp 156/255 strides, 236/335 rejected PASS","godot netbridge/equip_model/remote_player/mob_view PASS"],"commit":"2D-proto"} diff --git a/audit/port-map/EterBase/CRC32.cpp.json b/audit/port-map/EterBase/CRC32.cpp.json new file mode 100644 index 00000000..99bba090 --- /dev/null +++ b/audit/port-map/EterBase/CRC32.cpp.json @@ -0,0 +1,28 @@ +{ + "reference": "EterBase/CRC32.cpp", + "reference_sha256": "80bbbc71a273097830279d96c7d7ac28df6d043675711eb1b6f28a9801a83016", + "priority": "P4", + "contracts": [], + "functions": { + "GetCRC32": { + "impl": [ + "extension/src/platform/EterBase/CRC32.cpp:GetCRC32" + ], + "status": "ADAPTED", + "note": "platform CRC32 table implementation; same polynomial and GetCRC32 values (checked against every stored filename_crc)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "GetCaseCRC32": { + "status": "TODO" + }, + "GetHFILECRC32": { + "status": "TODO" + }, + "GetFileCRC32": { + "status": "TODO" + }, + "GetFileSize": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/EterBase/FileBase.cpp.json b/audit/port-map/EterBase/FileBase.cpp.json new file mode 100644 index 00000000..7d984595 --- /dev/null +++ b/audit/port-map/EterBase/FileBase.cpp.json @@ -0,0 +1,72 @@ +{ + "reference": "EterBase/FileBase.cpp", + "reference_sha256": "262fcad3ae6ec2ee5c988c1beea0cfa2cc2adffc5e544fe36885bef1dc6dc71d", + "priority": "P4", + "contracts": [], + "functions": { + "CFileBase::CFileBase": { + "impl": [ + "extension/src/platform/EterBase/FileBase.cpp:CFileBase::CFileBase" + ], + "status": "ADAPTED", + "note": "platform CFileBase over std::fstream; open falls back to a case-insensitive path match (NTFS spelling)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CFileBase::~CFileBase": { + "impl": [ + "extension/src/platform/EterBase/FileBase.cpp:CFileBase::~CFileBase" + ], + "status": "ADAPTED", + "note": "platform CFileBase over std::fstream; open falls back to a case-insensitive path match (NTFS spelling)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CFileBase::GetFileName": { + "status": "TODO" + }, + "CFileBase::Destroy": { + "impl": [ + "extension/src/platform/EterBase/FileBase.cpp:CFileBase::Destroy" + ], + "status": "ADAPTED", + "note": "platform CFileBase over std::fstream; open falls back to a case-insensitive path match (NTFS spelling)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CFileBase::Close": { + "impl": [ + "extension/src/platform/EterBase/FileBase.cpp:CFileBase::Close" + ], + "status": "ADAPTED", + "note": "platform CFileBase over std::fstream; open falls back to a case-insensitive path match (NTFS spelling)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CFileBase::Create": { + "impl": [ + "extension/src/platform/EterBase/FileBase.cpp:CFileBase::Create" + ], + "status": "ADAPTED", + "note": "platform CFileBase over std::fstream; open falls back to a case-insensitive path match (NTFS spelling)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CFileBase::Size": { + "status": "TODO" + }, + "CFileBase::SeekCur": { + "status": "TODO" + }, + "CFileBase::Seek": { + "status": "TODO" + }, + "CFileBase::GetPosition": { + "status": "TODO" + }, + "CFileBase::Write": { + "status": "TODO" + }, + "CFileBase::Read": { + "status": "TODO" + }, + "CFileBase::IsNull": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/EterBase/FileLoader.cpp.json b/audit/port-map/EterBase/FileLoader.cpp.json new file mode 100644 index 00000000..1b56416f --- /dev/null +++ b/audit/port-map/EterBase/FileLoader.cpp.json @@ -0,0 +1,102 @@ +{ + "reference": "EterBase/FileLoader.cpp", + "reference_sha256": "aaf832b188b7fff6350c7e3a41147fa4abfa2873ce160022c433dcac848c6875", + "priority": "P4", + "contracts": [], + "functions": { + "CMemoryTextFileLoader::CMemoryTextFileLoader": { + "impl": [ + "extension/src/platform/EterBase/FileLoader.cpp:CMemoryTextFileLoader::CMemoryTextFileLoader" + ], + "status": "ADAPTED", + "note": "40250 file verbatim, one change: the high-bit test casts to signed char (char is unsigned on arm64)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMemoryTextFileLoader::~CMemoryTextFileLoader": { + "impl": [ + "extension/src/platform/EterBase/FileLoader.cpp:CMemoryTextFileLoader::~CMemoryTextFileLoader" + ], + "status": "ADAPTED", + "note": "40250 file verbatim, one change: the high-bit test casts to signed char (char is unsigned on arm64)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMemoryTextFileLoader::SplitLineByTab": { + "status": "TODO" + }, + "CMemoryTextFileLoader::SplitLine2": { + "status": "TODO" + }, + "CMemoryTextFileLoader::SplitLine": { + "status": "TODO" + }, + "CMemoryTextFileLoader::GetLineCount": { + "impl": [ + "extension/src/platform/EterBase/FileLoader.cpp:CMemoryTextFileLoader::GetLineCount" + ], + "status": "ADAPTED", + "note": "40250 file verbatim, one change: the high-bit test casts to signed char (char is unsigned on arm64)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMemoryTextFileLoader::CheckLineIndex": { + "status": "TODO" + }, + "CMemoryTextFileLoader::GetLineString": { + "impl": [ + "extension/src/platform/EterBase/FileLoader.cpp:CMemoryTextFileLoader::GetLineString" + ], + "status": "ADAPTED", + "note": "40250 file verbatim, one change: the high-bit test casts to signed char (char is unsigned on arm64)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMemoryTextFileLoader::Bind": { + "impl": [ + "extension/src/platform/EterBase/FileLoader.cpp:CMemoryTextFileLoader::Bind" + ], + "status": "ADAPTED", + "note": "40250 file verbatim, one change: the high-bit test casts to signed char (char is unsigned on arm64)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMemoryFileLoader::GetSize": { + "status": "TODO" + }, + "CMemoryFileLoader::GetPosition": { + "status": "TODO" + }, + "CMemoryFileLoader::IsReadableSize": { + "status": "TODO" + }, + "CMemoryFileLoader::Read": { + "status": "TODO" + }, + "CMemoryFileLoader::GetCurrentPositionPointer": { + "status": "TODO" + }, + "CMemoryFileLoader::CMemoryFileLoader": { + "status": "TODO" + }, + "CMemoryFileLoader::~CMemoryFileLoader": { + "status": "TODO" + }, + "CDiskFileLoader::GetSize": { + "status": "TODO" + }, + "CDiskFileLoader::Read": { + "status": "TODO" + }, + "CDiskFileLoader::Open": { + "status": "TODO" + }, + "CDiskFileLoader::Close": { + "status": "TODO" + }, + "CDiskFileLoader::Initialize": { + "status": "TODO" + }, + "CDiskFileLoader::CDiskFileLoader": { + "status": "TODO" + }, + "CDiskFileLoader::~CDiskFileLoader": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/EterBase/MappedFile.cpp.json b/audit/port-map/EterBase/MappedFile.cpp.json new file mode 100644 index 00000000..e61c1fab --- /dev/null +++ b/audit/port-map/EterBase/MappedFile.cpp.json @@ -0,0 +1,104 @@ +{ + "reference": "EterBase/MappedFile.cpp", + "reference_sha256": "82d65b1bb2bdc91038be929e079212af33cf963e5d4722d10256bd1df4341ee9", + "priority": "P4", + "contracts": [], + "functions": { + "CMappedFile::CMappedFile": { + "impl": [ + "extension/src/platform/EterBase/MappedFile.cpp:CMappedFile::CMappedFile" + ], + "status": "ADAPTED", + "note": "platform CMappedFile: std::ifstream read into an owned buffer instead of CreateFileMapping/MapViewOfFile; same Create/Map/Link/BindLZObject contract", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMappedFile::~CMappedFile": { + "impl": [ + "extension/src/platform/EterBase/MappedFile.cpp:CMappedFile::~CMappedFile" + ], + "status": "ADAPTED", + "note": "platform CMappedFile: std::ifstream read into an owned buffer instead of CreateFileMapping/MapViewOfFile; same Create/Map/Link/BindLZObject contract", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMappedFile::Create": { + "impl": [ + "extension/src/platform/EterBase/MappedFile.cpp:CMappedFile::Create" + ], + "status": "ADAPTED", + "note": "platform CMappedFile: std::ifstream read into an owned buffer instead of CreateFileMapping/MapViewOfFile; same Create/Map/Link/BindLZObject contract", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMappedFile::Get": { + "status": "TODO" + }, + "CMappedFile::Link": { + "impl": [ + "extension/src/platform/EterBase/MappedFile.cpp:CMappedFile::Link" + ], + "status": "ADAPTED", + "note": "platform CMappedFile: std::ifstream read into an owned buffer instead of CreateFileMapping/MapViewOfFile; same Create/Map/Link/BindLZObject contract", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMappedFile::BindLZObject": { + "impl": [ + "extension/src/platform/EterBase/MappedFile.cpp:CMappedFile::BindLZObject" + ], + "status": "ADAPTED", + "note": "platform CMappedFile: std::ifstream read into an owned buffer instead of CreateFileMapping/MapViewOfFile; same Create/Map/Link/BindLZObject contract", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMappedFile::BindLZObjectWithBufferedSize": { + "status": "TODO" + }, + "CMappedFile::AppendDataBlock": { + "status": "TODO" + }, + "CMappedFile::Destroy": { + "impl": [ + "extension/src/platform/EterBase/MappedFile.cpp:CMappedFile::Destroy" + ], + "status": "ADAPTED", + "note": "platform CMappedFile: std::ifstream read into an owned buffer instead of CreateFileMapping/MapViewOfFile; same Create/Map/Link/BindLZObject contract", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMappedFile::Seek": { + "status": "TODO" + }, + "CMappedFile::Map": { + "impl": [ + "extension/src/platform/EterBase/MappedFile.cpp:CMappedFile::Map" + ], + "status": "ADAPTED", + "note": "platform CMappedFile: std::ifstream read into an owned buffer instead of CreateFileMapping/MapViewOfFile; same Create/Map/Link/BindLZObject contract", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMappedFile::GetCurrentSeekPoint": { + "status": "TODO" + }, + "CMappedFile::Size": { + "impl": [ + "extension/src/platform/EterBase/MappedFile.cpp:CMappedFile::Size" + ], + "status": "ADAPTED", + "note": "platform CMappedFile: std::ifstream read into an owned buffer instead of CreateFileMapping/MapViewOfFile; same Create/Map/Link/BindLZObject contract", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + }, + "CMappedFile::GetPosition": { + "status": "TODO" + }, + "CMappedFile::Read": { + "status": "TODO" + }, + "CMappedFile::GetSeekPosition": { + "status": "TODO" + }, + "CMappedFile::Unmap": { + "impl": [ + "extension/src/platform/EterBase/MappedFile.cpp:CMappedFile::Unmap" + ], + "status": "ADAPTED", + "note": "platform CMappedFile: std::ifstream read into an owned buffer instead of CreateFileMapping/MapViewOfFile; same Create/Map/Link/BindLZObject contract", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects)" + } + } +} diff --git a/audit/port-map/EterBase/Stl.cpp.json b/audit/port-map/EterBase/Stl.cpp.json new file mode 100644 index 00000000..dba73a1c --- /dev/null +++ b/audit/port-map/EterBase/Stl.cpp.json @@ -0,0 +1,26 @@ +{ + "reference": "EterBase/Stl.cpp", + "reference_sha256": "239c44c77e8dba08e91f8838447ad2f15ab19ac6328650d13da37b9600a249cd", + "priority": "P4", + "contracts": [], + "functions": { + "korean_tolower": { + "impl": [ + "extension/src/port/EterBase/Stl.cpp:korean_tolower" + ], + "status": "PORTED" + }, + "stl_static_string": { + "status": "TODO" + }, + "stl_lowers": { + "impl": [ + "extension/src/port/EterBase/Stl.cpp:stl_lowers" + ], + "status": "PORTED" + }, + "split_string": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/EterBase/lzo.cpp.json b/audit/port-map/EterBase/lzo.cpp.json new file mode 100644 index 00000000..d2b616b8 --- /dev/null +++ b/audit/port-map/EterBase/lzo.cpp.json @@ -0,0 +1,98 @@ +{ + "reference": "EterBase/lzo.cpp", + "reference_sha256": "7a236172e9900733e119944ffabf6762a999d4346fde9241e3027836d72c1354", + "priority": "P4", + "contracts": [], + "functions": { + "CLZObject::CLZObject": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZObject::CLZObject" + ], + "status": "PORTED" + }, + "CLZObject::Initialize": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZObject::Initialize" + ], + "status": "PORTED" + }, + "CLZObject::Clear": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZObject::Clear" + ], + "status": "PORTED" + }, + "CLZObject::~CLZObject": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZObject::~CLZObject" + ], + "status": "PORTED" + }, + "CLZObject::GetSize": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZObject::GetSize" + ], + "status": "PORTED" + }, + "CLZObject::BeginCompress": { + "status": "TODO" + }, + "CLZObject::BeginCompressInBuffer": { + "status": "TODO" + }, + "CLZObject::Compress": { + "status": "TODO" + }, + "CLZObject::BeginDecompress": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZObject::BeginDecompress" + ], + "status": "PORTED" + }, + "CLZObject::Decompress": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZObject::Decompress" + ], + "status": "PORTED" + }, + "CLZObject::Encrypt": { + "status": "TODO" + }, + "CLZObject::__Decrypt": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZObject::__Decrypt" + ], + "status": "PORTED" + }, + "CLZObject::AllocBuffer": { + "status": "TODO" + }, + "CLZO::CLZO": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZO::CLZO" + ], + "status": "PORTED" + }, + "CLZO::~CLZO": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZO::~CLZO" + ], + "status": "PORTED" + }, + "CLZO::CompressMemory": { + "status": "TODO" + }, + "CLZO::CompressEncryptedMemory": { + "status": "TODO" + }, + "CLZO::Decompress": { + "impl": [ + "extension/src/port/EterBase/lzo.cpp:CLZO::Decompress" + ], + "status": "PORTED" + }, + "CLZO::GetWorkMemory": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/EterBase/tea.cpp.json b/audit/port-map/EterBase/tea.cpp.json new file mode 100644 index 00000000..c1439a71 --- /dev/null +++ b/audit/port-map/EterBase/tea.cpp.json @@ -0,0 +1,26 @@ +{ + "reference": "EterBase/tea.cpp", + "reference_sha256": "aeeb57c96a9febd22d11e221462f5f6b18830370847f45e74eec5fb085988007", + "priority": "P4", + "contracts": [], + "functions": { + "tea_code": { + "status": "TODO" + }, + "tea_decode": { + "impl": [ + "extension/src/port/EterBase/tea.cpp:tea_decode" + ], + "status": "PORTED" + }, + "tea_encrypt": { + "status": "TODO" + }, + "tea_decrypt": { + "impl": [ + "extension/src/port/EterBase/tea.cpp:tea_decrypt" + ], + "status": "PORTED" + } + } +} diff --git a/audit/port-map/EterPack/EterPack.cpp.json b/audit/port-map/EterPack/EterPack.cpp.json new file mode 100644 index 00000000..cdaee06a --- /dev/null +++ b/audit/port-map/EterPack/EterPack.cpp.json @@ -0,0 +1,218 @@ +{ + "reference": "EterPack/EterPack.cpp", + "reference_sha256": "7a38554a275da2fe7d16ea4c60a3ab644dbd3ed7845d19c385819045e74e6787", + "priority": "P4", + "contracts": [], + "functions": { + "CMakePackLog::SetFileName": { + "status": "TODO" + }, + "CMakePackLog::GetSingleton": { + "status": "TODO" + }, + "CMakePackLog::CMakePackLog": { + "status": "TODO" + }, + "CMakePackLog::~CMakePackLog": { + "status": "TODO" + }, + "CMakePackLog::__IsLogMode": { + "status": "TODO" + }, + "CMakePackLog::Writef": { + "status": "TODO" + }, + "CMakePackLog::Writenf": { + "status": "TODO" + }, + "CMakePackLog::Write": { + "status": "TODO" + }, + "CMakePackLog::__Write": { + "status": "TODO" + }, + "CMakePackLog::WriteErrorf": { + "status": "TODO" + }, + "CMakePackLog::WriteErrornf": { + "status": "TODO" + }, + "CMakePackLog::WriteError": { + "status": "TODO" + }, + "CMakePackLog::__WriteError": { + "status": "TODO" + }, + "CMakePackLog::FlushError": { + "status": "TODO" + }, + "CEterPack::CEterPack": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::CEterPack" + ], + "status": "PORTED" + }, + "CEterPack::~CEterPack": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::~CEterPack" + ], + "status": "PORTED" + }, + "CEterPack::Destroy": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::Destroy" + ], + "status": "PORTED" + }, + "CEterPack::GetPathName": { + "status": "TODO" + }, + "CEterPack::Create": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::Create" + ], + "status": "PORTED" + }, + "CEterPack::DecryptIV": { + "status": "TODO" + }, + "CEterPack::DecryptIndexFile": { + "status": "TODO" + }, + "CEterPack::EncryptIndexFile": { + "status": "TODO" + }, + "CEterPack::__BuildIndex": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::__BuildIndex" + ], + "status": "PORTED" + }, + "CEterPack::Get": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::Get" + ], + "status": "PORTED" + }, + "CEterPack::Get2": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::Get2" + ], + "status": "PORTED" + }, + "CEterPack::Delete": { + "status": "TODO" + }, + "CEterPack::Extract": { + "status": "TODO" + }, + "CEterPack::GetNames": { + "status": "TODO" + }, + "CEterPack::Put": { + "status": "TODO" + }, + "CEterPack::GenerateMD5Hash": { + "status": "TODO" + }, + "CEterPack::GetFragmentSize": { + "status": "TODO" + }, + "CEterPack::CreateIndexFile": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::CreateIndexFile" + ], + "status": "PORTED" + }, + "CEterPack::WriteIndex": { + "status": "TODO" + }, + "CEterPack::GetFreeBlockIndex": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::GetFreeBlockIndex" + ], + "status": "PORTED" + }, + "CEterPack::PushFreeIndex": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::PushFreeIndex" + ], + "status": "PORTED" + }, + "CEterPack::GetNewIndexPosition": { + "status": "TODO" + }, + "CEterPack::NewIndex": { + "status": "TODO" + }, + "CEterPack::FindIndex": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::FindIndex" + ], + "status": "PORTED" + }, + "CEterPack::IsExist": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::IsExist" + ], + "status": "PORTED" + }, + "CEterPack::CreateDataFile": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::CreateDataFile" + ], + "status": "PORTED" + }, + "CEterPack::GetNewDataPosition": { + "status": "TODO" + }, + "CEterPack::ReadData": { + "status": "TODO" + }, + "CEterPack::WriteData": { + "status": "TODO" + }, + "CEterPack::WriteNewData": { + "status": "TODO" + }, + "CEterPack::GetIndexMap": { + "status": "TODO" + }, + "CEterPack::DeleteUnreferencedData": { + "status": "TODO" + }, + "CEterPack::GetDBName": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterPack::GetDBName" + ], + "status": "PORTED" + }, + "CEterPack::__CreateFileNameKey_Panama": { + "status": "TODO" + }, + "CEterPack::__Encrypt_Panama": { + "status": "TODO" + }, + "CEterPack::__Decrypt_Panama": { + "status": "TODO" + }, + "CEterPack::GetPackPolicy_HybridCrypt": { + "status": "TODO" + }, + "CEterFileDict::InsertItem": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterFileDict::InsertItem" + ], + "status": "PORTED" + }, + "CEterFileDict::UpdateItem": { + "status": "TODO" + }, + "CEterFileDict::GetItem": { + "impl": [ + "extension/src/port/EterPack/EterPack.cpp:CEterFileDict::GetItem" + ], + "status": "PORTED" + } + } +} diff --git a/audit/port-map/EterPack/EterPackManager.cpp.json b/audit/port-map/EterPack/EterPackManager.cpp.json new file mode 100644 index 00000000..029a70f0 --- /dev/null +++ b/audit/port-map/EterPack/EterPackManager.cpp.json @@ -0,0 +1,128 @@ +{ + "reference": "EterPack/EterPackManager.cpp", + "reference_sha256": "335a4bf569f4cf0b2154f39ca35f1e294f1a514641e082290f1f1ce6c0a336a9", + "priority": "P4", + "contracts": [], + "functions": { + "CEterPackManager::FindPack": { + "status": "TODO" + }, + "CEterPackManager::SetCacheMode": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::SetCacheMode" + ], + "status": "PORTED" + }, + "CEterPackManager::SetRelativePathMode": { + "status": "TODO" + }, + "CEterPackManager::ConvertFileName": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::ConvertFileName" + ], + "status": "PORTED" + }, + "CEterPackManager::CompareName": { + "status": "TODO" + }, + "CEterPackManager::LoadStaticCache": { + "status": "TODO" + }, + "CEterPackManager::__FindCache": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::__FindCache" + ], + "status": "PORTED" + }, + "CEterPackManager::__ClearCacheMap": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::__ClearCacheMap" + ], + "status": "PORTED" + }, + "CEterPackManager::Get": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::Get" + ], + "status": "PORTED" + }, + "CEterPackManager::GetFromPack": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::GetFromPack" + ], + "status": "PORTED" + }, + "CEterPackManager::ArrangeMemoryMappedPack": { + "status": "TODO" + }, + "CEterPackManager::GetFromFile": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::GetFromFile" + ], + "status": "PORTED" + }, + "CEterPackManager::isExistInPack": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::isExistInPack" + ], + "status": "PORTED" + }, + "CEterPackManager::isExist": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::isExist" + ], + "status": "PORTED" + }, + "CEterPackManager::RegisterRootPack": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::RegisterRootPack" + ], + "status": "PORTED" + }, + "CEterPackManager::GetRootPackFileName": { + "status": "TODO" + }, + "CEterPackManager::DecryptPackIV": { + "status": "TODO" + }, + "CEterPackManager::RegisterPackWhenPackMaking": { + "status": "TODO" + }, + "CEterPackManager::RegisterPack": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::RegisterPack" + ], + "status": "PORTED" + }, + "CEterPackManager::SetSearchMode": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::SetSearchMode" + ], + "status": "PORTED" + }, + "CEterPackManager::GetSearchMode": { + "status": "TODO" + }, + "CEterPackManager::CEterPackManager": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::CEterPackManager" + ], + "status": "PORTED" + }, + "CEterPackManager::~CEterPackManager": { + "impl": [ + "extension/src/port/EterPack/EterPackManager.cpp:CEterPackManager::~CEterPackManager" + ], + "status": "PORTED" + }, + "CEterPackManager::RetrieveHybridCryptPackKeys": { + "status": "TODO" + }, + "CEterPackManager::RetrieveHybridCryptPackSDB": { + "status": "TODO" + }, + "CEterPackManager::WriteHybridCryptPackInfo": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/EterPack/EterPackPolicy_CSHybridCrypt.cpp.json b/audit/port-map/EterPack/EterPackPolicy_CSHybridCrypt.cpp.json new file mode 100644 index 00000000..166b0630 --- /dev/null +++ b/audit/port-map/EterPack/EterPackPolicy_CSHybridCrypt.cpp.json @@ -0,0 +1,53 @@ +{ + "reference": "EterPack/EterPackPolicy_CSHybridCrypt.cpp", + "reference_sha256": "02db1037e746468b036a3ec2004a4ebffa568d5433fe6e94bc50729ed69e6e0b", + "priority": "P4", + "contracts": [], + "functions": { + "GetFileExt": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::~EterPackPolicy_CSHybridCrypt": { + "impl": [ + "extension/src/port/EterPack/EterPackPolicy_CSHybridCrypt.cpp:EterPackPolicy_CSHybridCrypt::~EterPackPolicy_CSHybridCrypt" + ], + "status": "PORTED" + }, + "EterPackPolicy_CSHybridCrypt::IsContainingCryptKey": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::GenerateCryptKey": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::GetPerFileCryptKey": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::EncryptMemory": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::DecryptMemory": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::WriteCryptKeyToFile": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::ReadCryptKeyInfoFromStream": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::GenerateSupplementaryDataBlock": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::GetSupplementaryDataBlock": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::IsContainingSDBFile": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::WriteSupplementaryDataBlockToFile": { + "status": "TODO" + }, + "EterPackPolicy_CSHybridCrypt::ReadSupplementatyDataBlockFromStream": { + "status": "TODO" + } + } +} diff --git a/audit/port-map/UserInterface/UserInterface.cpp.json b/audit/port-map/UserInterface/UserInterface.cpp.json new file mode 100644 index 00000000..e28e1a91 --- /dev/null +++ b/audit/port-map/UserInterface/UserInterface.cpp.json @@ -0,0 +1,73 @@ +{ + "reference": "UserInterface/UserInterface.cpp", + "reference_sha256": "beed987e4b1de627acafe7ea6f5fde989b8f4b33c8643fe2abb0f7152ea4a9eb", + "priority": "P4", + "contracts": [], + "functions": { + "ApplicationSetErrorString": { + "status": "TODO" + }, + "CheckPythonLibraryFilenames": { + "status": "TODO" + }, + "ApplicationStringTable_Initialize": { + "status": "TODO" + }, + "ApplicationStringTable_GetString": { + "status": "TODO" + }, + "ApplicationStringTable_GetStringz": { + "status": "TODO" + }, + "PackInitialize": { + "impl": [ + "extension/src/platform/UserInterface/UserInterface.cpp:PackInitialize" + ], + "status": "ADAPTED", + "note": "platform PackInitialize: Index pack names resolved to the on-disk .eix spelling case-insensitively; no LogBoxf; CTextFileLoader::SetCacheMode / CSoundData::SetPackMode not called (units not ported, neither affects pack registration)", + "test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects); extension/tests/proto_test.cpp (40250 locale/en item_proto/mob_proto through pack://)" + }, + "RunMainScript": { + "status": "TODO" + }, + "Main": { + "status": "TODO" + }, + "CreateMetin2GameMutex": { + "status": "TODO" + }, + "DestroyMetin2GameMutex": { + "status": "TODO" + }, + "__ErrorPythonLibraryIsNotExist": { + "status": "TODO" + }, + "__IsTimeStampOption": { + "status": "TODO" + }, + "__PrintTimeStamp": { + "status": "TODO" + }, + "__IsLocaleOption": { + "status": "TODO" + }, + "__IsLocaleVersion": { + "status": "TODO" + }, + "__IsOpenIDAuthKeyOption": { + "status": "TODO" + }, + "__IsOpenIDTestOption": { + "status": "TODO" + }, + "WinMain": { + "status": "TODO" + }, + "GrannyError": { + "status": "TODO" + }, + "Setup": { + "status": "TODO" + } + } +} diff --git a/extension/CMakeLists.txt b/extension/CMakeLists.txt index 4c351f24..de972969 100644 --- a/extension/CMakeLists.txt +++ b/extension/CMakeLists.txt @@ -50,10 +50,10 @@ target_include_directories(mtpack PUBLIC src/pack) target_link_libraries(mtpack PUBLIC mt3p::sodium mt3p::zstd xrender::formats) target_compile_features(mtpack PUBLIC cxx_std_20) -# --- mtproto: item_proto / mob_proto reader (libsodium + LZO) --- +# --- mtproto: item_proto / mob_proto reader, 40250 format (CLZO/TEA from port_platform) --- add_library(mtproto STATIC src/proto/proto.cpp) target_include_directories(mtproto PUBLIC src/proto) -target_link_libraries(mtproto PUBLIC mt3p::sodium mt3p::minilzo) +target_link_libraries(mtproto PUBLIC port_platform) target_compile_features(mtproto PUBLIC cxx_std_20) # --- port_logic + port_platform: 40250 mirror (extension/src/port) and its platform layer @@ -146,10 +146,10 @@ if(BUILD_TESTING AND MT_HOST_BUILD) add_executable(proto_test tests/proto_test.cpp) target_link_libraries(proto_test PRIVATE mtproto) - add_test(NAME proto.item_mob COMMAND $) - if(DEFINED ENV{M2_ASSETS}) - set_tests_properties(proto.item_mob PROPERTIES ENVIRONMENT "M2_ASSETS=$ENV{M2_ASSETS}") - endif() + # MT_40250_CLIENT comes from src/port/CMakeLists.txt; skipped (77) without the client, failed under + # MT_ASSETS_STRICT=1. + add_test(NAME proto.item_mob COMMAND $ ${MT_40250_CLIENT}) + set_tests_properties(proto.item_mob PROPERTIES SKIP_RETURN_CODE 77) add_executable(proto_item_layout_test tests/proto_item_layout_test.cpp) target_link_libraries(proto_item_layout_test PRIVATE mtproto) diff --git a/extension/src/platform/UserInterface/UserInterface.h b/extension/src/platform/UserInterface/UserInterface.h index 2cc01477..f439b488 100644 --- a/extension/src/platform/UserInterface/UserInterface.h +++ b/extension/src/platform/UserInterface/UserInterface.h @@ -2,8 +2,9 @@ // Platform-layer pieces of 40250 UserInterface/UserInterface.cpp (the WinMain unit). 40250 declares these nowhere: // Main() in the same file calls them. The extension's pack bootstrap calls them instead. -// 40250: PackInitialize. Registers /Index the way the 40250 client does at startup. The CLZO and -// CEterPackManager singletons it needs must already exist (see PackSingletons). +// 40250: PackInitialize +// Registers /Index the way the 40250 client does at startup. The CLZO and CEterPackManager singletons +// it needs must already exist (see PackSingletons). bool PackInitialize(const char * c_pszFolder); // The two function-static singletons 40250 Main() creates before PackInitialize("pack") diff --git a/extension/src/port/GameLib/ItemData.h b/extension/src/port/GameLib/ItemData.h index 1c5f8afe..79772095 100644 --- a/extension/src/port/GameLib/ItemData.h +++ b/extension/src/port/GameLib/ItemData.h @@ -369,13 +369,13 @@ class CItemData typedef struct SItemLimit { BYTE bType; - long lValue; + LONG lValue; // PORT: fixed-width Win32 long (item_proto record layout) } TItemLimit; typedef struct SItemApply { BYTE bType; - long lValue; + LONG lValue; // PORT: fixed-width Win32 long (item_proto record layout) } TItemApply; typedef struct SItemTable @@ -400,14 +400,15 @@ class CItemData TItemLimit aLimits[ITEM_LIMIT_MAX_NUM]; TItemApply aApplies[ITEM_APPLY_MAX_NUM]; - long alValues[ITEM_VALUES_MAX_NUM]; - long alSockets[ITEM_SOCKET_MAX_NUM]; + LONG alValues[ITEM_VALUES_MAX_NUM]; // PORT: fixed-width Win32 long + LONG alSockets[ITEM_SOCKET_MAX_NUM]; // PORT: fixed-width Win32 long DWORD dwRefinedVnum; WORD wRefineSet; BYTE bAlterToMagicItemPct; BYTE bSpecular; BYTE bGainSocketPct; } TItemTable; + static_assert(sizeof(SItemTable) == 156, "item_proto MIPX stride"); // PORT: 40250 ILP32 layout // typedef struct SItemTable // { diff --git a/extension/src/proto/proto.cpp b/extension/src/proto/proto.cpp index 0b7cfcfb..e7574c41 100644 --- a/extension/src/proto/proto.cpp +++ b/extension/src/proto/proto.cpp @@ -1,22 +1,87 @@ #include "proto.h" -#include +#include "EterBase/StdAfx.h" +#include "EterBase/lzo.h" +#include "GameLib/StdAfx.h" +#include "GameLib/ItemData.h" +#include "../platform/UserInterface/UserInterface.h" #include #include -extern "C" { -#include -} - namespace mtproto { namespace { -constexpr uint32_t FOURCC_MIPX = 0x5850494D; // "MIPX" bytes 4D 49 50 58 (LE u32) -constexpr uint32_t FOURCC_MIPT = 0x5450494D; // "MIPT" -constexpr uint32_t FOURCC_MMPT = 0x54504D4D; // "MMPT" -constexpr uint32_t FOURCC_MCOZ = 0x5A4F434D; // "MCOZ" +// CPythonNonPlayer::SMobTable (UserInterface/PythonNonPlayer.h), the 40250 ILP32 layout. The mirror of that header +// needs UserInterface/StdAfx.h, which includes the Python headers (after batch 2P); until then the table is here. +#pragma pack(push, 1) +struct SMobSkillLevel { + DWORD dwVnum; + BYTE bLevel; +}; + +struct SMobTable { + DWORD dwVnum; + char szName[24 + 1]; // CHARACTER_NAME_MAX_LEN (UserInterface/StdAfx.h) + char szLocaleName[24 + 1]; + + BYTE bType; + BYTE bRank; + BYTE bBattleType; + BYTE bLevel; + BYTE bSize; + + DWORD dwGoldMin; + DWORD dwGoldMax; + DWORD dwExp; + DWORD dwMaxHP; + BYTE bRegenCycle; + BYTE bRegenPercent; + WORD wDef; + + DWORD dwAIFlag; + DWORD dwRaceFlag; + DWORD dwImmuneFlag; + + BYTE bStr, bDex, bCon, bInt; + DWORD dwDamageRange[2]; + + short sAttackSpeed; + short sMovingSpeed; + BYTE bAggresiveHPPct; + WORD wAggressiveSight; + WORD wAttackRange; + + char cEnchants[6]; // MOB_ENCHANTS_MAX_NUM + char cResists[11]; // MOB_RESISTS_MAX_NUM + + DWORD dwResurrectionVnum; + DWORD dwDropItemVnum; + + BYTE bMountCapacity; + BYTE bOnClickType; + + BYTE bEmpire; + char szFolder[64 + 1]; + float fDamMultiply; + DWORD dwSummonVnum; + DWORD dwDrainSP; + DWORD dwMonsterColor; + DWORD dwPolymorphItemVnum; + + SMobSkillLevel Skills[5]; // MOB_SKILL_MAX_NUM + + BYTE bBerserkPoint; + BYTE bStoneSkinPoint; + BYTE bGodSpeedPoint; + BYTE bDeathBlowPoint; + BYTE bRevivePoint; +}; +#pragma pack(pop) +static_assert(sizeof(SMobTable) == 255, "mob_proto record"); + +using TItemTable = CItemData::TItemTable; uint32_t rd_u32(const uint8_t *p) { uint32_t v; @@ -24,81 +89,12 @@ uint32_t rd_u32(const uint8_t *p) { return v; } -// The fork's tea_decrypt (EterBase/tea.cpp): XChaCha20 with key/nonce derived from -// the 16-byte input key via BLAKE2b. size is rounded up to a multiple of 8. -void tea_decrypt(uint8_t *dst, const uint8_t *src, const std::array &key32, - size_t size) { - uint8_t key16[16]; - std::memcpy(key16, key32.data(), 16); // 4 LE dwords -> raw bytes - - uint8_t dkey[crypto_stream_xchacha20_KEYBYTES]; - uint8_t nonce[crypto_stream_xchacha20_NONCEBYTES]; - crypto_generichash(dkey, sizeof(dkey), key16, 16, - reinterpret_cast("M2DevPackEncrypt"), 16); - uint8_t nonce_seed[crypto_stream_xchacha20_NONCEBYTES + 8]; - crypto_generichash(nonce_seed, sizeof(nonce_seed), key16, 16, - reinterpret_cast("M2DevNonce"), 10); - std::memcpy(nonce, nonce_seed, sizeof(nonce)); - - size_t rs = (size % 8 == 0) ? size : size + 8 - (size % 8); - crypto_stream_xchacha20_xor(dst, src, rs, nonce, dkey); - sodium_memzero(dkey, sizeof(dkey)); -} - -// CLZO container -> decompressed bytes. -bool clzo_decompress(const uint8_t *blob, size_t blob_len, const std::array &key, - std::vector &out, std::string *err) { - if (blob_len < 20 || rd_u32(blob) != FOURCC_MCOZ) { - if (err) *err = "CLZO: bad MCOZ header"; - return false; - } - const uint32_t enc_size = rd_u32(blob + 4); - const uint32_t comp_size = rd_u32(blob + 8); - const uint32_t real_size = rd_u32(blob + 12); - - out.assign(real_size, 0); - lzo_uint out_len = real_size; - int r; - - if (enc_size > 0) { - size_t rs = (enc_size % 8 == 0) ? enc_size : enc_size + 8 - (enc_size % 8); - if (16 + rs > blob_len) { - if (err) *err = "CLZO: encrypted region past end"; - return false; - } - std::vector dec(rs); - tea_decrypt(dec.data(), blob + 16, key, enc_size); // src = blob+16 (== m_pbIn-4) - if (rd_u32(dec.data()) != FOURCC_MCOZ) { - if (err) *err = "CLZO: wrong key (inner MCOZ mismatch)"; - return false; - } - r = lzo1x_decompress_safe(dec.data() + 4, comp_size, out.data(), &out_len, nullptr); - } else { - if (20u + comp_size > blob_len) { - if (err) *err = "CLZO: compressed region past end"; - return false; - } - r = lzo1x_decompress_safe(blob + 20, comp_size, out.data(), &out_len, nullptr); - } - - if (r != LZO_E_OK) { - if (err) *err = "CLZO: lzo1x_decompress_safe failed (" + std::to_string(r) + ")"; - return false; - } - if (out_len != real_size) { - if (err) *err = "CLZO: size mismatch " + std::to_string(out_len) + " != " + - std::to_string(real_size); - return false; - } - return true; -} - -std::string cstr(const uint8_t *p, size_t maxn) { +std::string cstr(const char *p, size_t maxn) { size_t n = 0; while (n < maxn && p[n]) { ++n; } - return std::string(reinterpret_cast(p), n); + return std::string(p, n); } } // namespace @@ -114,131 +110,143 @@ bool load_proto(const std::string &path, const std::array &key, Pro return load_proto_bytes(file, key, out, err); } +// The header checks of CItemManager::LoadItemTable / CPythonNonPlayer::LoadNonPlayerData, then CLZO::Decompress. bool load_proto_bytes(const std::vector &file, const std::array &key, Proto &out, std::string *err) { - if (sodium_init() < 0) { - if (err) *err = "sodium_init failed"; - return false; - } - if (lzo_init() != LZO_E_OK) { - if (err) *err = "lzo_init failed"; - return false; - } - if (file.size() < 16) { + out = Proto{}; + if (file.size() < 12) { if (err) *err = "file too small"; return false; } - const uint8_t *p = file.data(); - out.fourcc = rd_u32(p); - p += 4; - - uint32_t data_size = 0; - if (out.fourcc == FOURCC_MIPX) { - out.version = rd_u32(p); - p += 4; - out.stride = rd_u32(p); - p += 4; - out.elements = rd_u32(p); - p += 4; - data_size = rd_u32(p); - p += 4; - if (out.version != 1) { - if (err) *err = "MIPX version != 1"; + size_t pos = 0; + auto read_u32 = [&](uint32_t &v) { + if (pos + 4 > file.size()) { return false; } - } else if (out.fourcc == FOURCC_MIPT || out.fourcc == FOURCC_MMPT) { - out.elements = rd_u32(p); - p += 4; - data_size = rd_u32(p); - p += 4; + v = rd_u32(file.data() + pos); + pos += 4; + return true; + }; + + read_u32(out.fourcc); + if (out.fourcc == MAKEFOURCC('M', 'I', 'P', 'X')) { + if (!read_u32(out.version) || !read_u32(out.stride)) { + if (err) *err = "truncated MIPX header"; + return false; + } + if (out.version != 1) { + if (err) *err = "invalid item_proto VERSION " + std::to_string(out.version); + return false; + } + if (out.stride != sizeof(TItemTable)) { + if (err) *err = "invalid item_proto STRIDE " + std::to_string(out.stride) + " != sizeof(SItemTable)"; + return false; + } + } else if (out.fourcc == MAKEFOURCC('M', 'I', 'P', 'T')) { + out.stride = sizeof(TItemTable); + } else if (out.fourcc == MAKEFOURCC('M', 'M', 'P', 'T')) { + out.stride = sizeof(SMobTable); } else { - if (err) *err = "unknown proto fourcc"; + if (err) *err = "invalid proto type"; return false; } - if (static_cast(p - file.data()) + data_size > file.size()) { + uint32_t data_size = 0; + if (!read_u32(out.elements) || !read_u32(data_size)) { + if (err) *err = "truncated proto header"; + return false; + } + // 40250 reads dwDataSize bytes without checking; CLZO then reads its own header lengths. + if (pos + data_size > file.size() || data_size < sizeof(CLZObject::THeader)) { if (err) *err = "declared data_size past end of file"; return false; } - if (!clzo_decompress(p, data_size, key, out.blob, err)) { + PackSingletons(); // CLZO, as 40250 Main() creates it + DWORD dwKey[4] = {key[0], key[1], key[2], key[3]}; + CLZObject zObj; + if (!CLZO::Instance().Decompress(zObj, file.data() + pos, dwKey)) { + if (err) *err = "CLZO::Decompress failed (wrong key or corrupt data)"; return false; } - if (out.elements == 0) { - if (err) *err = "0 elements"; + if (out.fourcc == MAKEFOURCC('M', 'M', 'P', 'T') && (zObj.GetSize() % sizeof(SMobTable)) != 0) { + if (err) *err = "invalid mob_proto size " + std::to_string(zObj.GetSize()) + ", check data format"; return false; } - if (out.stride == 0) { - if (out.blob.size() % out.elements != 0) { - if (err) *err = "blob not divisible by element count"; - return false; - } - out.stride = static_cast(out.blob.size() / out.elements); - } - if (static_cast(out.stride) * out.elements != out.blob.size()) { - if (err) *err = "stride * elements != blob size"; + // 40250 walks dwElements records from the buffer without comparing against its size. + if (static_cast(out.stride) * out.elements > zObj.GetSize()) { + if (err) *err = "elements * stride past decompressed size"; return false; } + const BYTE *buf = zObj.GetBuffer(); + out.blob.assign(buf, buf + static_cast(out.stride) * out.elements); return true; } ItemRecord parse_item(const uint8_t *r, uint32_t stride) { ItemRecord it; - if (!r || stride < 236) { + if (!r || stride != sizeof(TItemTable)) { return it; } - it.vnum = rd_u32(r + 0); - it.vnum_range = rd_u32(r + 4); - it.name = cstr(r + 8, 65); - it.locale_name = cstr(r + 73, 65); - it.type = r[138]; - it.sub_type = r[139]; - it.weight = r[140]; - it.size = r[141]; - it.anti_flags = rd_u32(r + 142); - it.flags = rd_u32(r + 146); - it.wear_flags = rd_u32(r + 150); - it.buy_price = rd_u32(r + 158); - it.sell_price = rd_u32(r + 162); - for (int i = 0; i < 2; ++i) { - const size_t off = 166 + static_cast(i) * 5; - it.limits[i].type = r[off]; - it.limits[i].value = static_cast(rd_u32(r + off + 1)); + TItemTable t; + std::memcpy(&t, r, sizeof(t)); + it.vnum = t.dwVnum; + it.vnum_range = t.dwVnumRange; + it.name = cstr(t.szName, sizeof(t.szName)); + it.locale_name = cstr(t.szLocaleName, sizeof(t.szLocaleName)); + it.type = t.bType; + it.sub_type = t.bSubType; + it.weight = t.bWeight; + it.size = t.bSize; + it.anti_flags = t.dwAntiFlags; + it.flags = t.dwFlags; + it.wear_flags = t.dwWearFlags; + it.immune_flag = t.dwImmuneFlag; + it.buy_price = t.dwIBuyItemPrice; + it.sell_price = t.dwISellItemPrice; + for (int i = 0; i < CItemData::ITEM_LIMIT_MAX_NUM; ++i) { + it.limits[i] = {t.aLimits[i].bType, t.aLimits[i].lValue}; } - for (int i = 0; i < 3; ++i) { - const size_t off = 176 + static_cast(i) * 5; - it.applies[i].type = r[off]; - it.applies[i].value = static_cast(rd_u32(r + off + 1)); + for (int i = 0; i < CItemData::ITEM_APPLY_MAX_NUM; ++i) { + it.applies[i] = {t.aApplies[i].bType, t.aApplies[i].lValue}; } - for (int i = 0; i < 6; ++i) { - it.values[i] = (int32_t)rd_u32(r + 191 + i * 4); + for (int i = 0; i < CItemData::ITEM_VALUES_MAX_NUM; ++i) { + it.values[i] = t.alValues[i]; } - for (int i = 0; i < 3; ++i) { - it.sockets[i] = static_cast(rd_u32(r + 215 + i * 4)); + for (int i = 0; i < CItemData::ITEM_SOCKET_MAX_NUM; ++i) { + it.sockets[i] = t.alSockets[i]; } - it.refined_vnum = rd_u32(r + 227); - it.refine_set = static_cast(r[231] | (static_cast(r[232]) << 8)); - it.alter_to_magic_pct = r[233]; - it.specular = r[234]; - it.gain_socket_pct = r[235]; + it.refined_vnum = t.dwRefinedVnum; + it.refine_set = t.wRefineSet; + it.alter_to_magic_pct = t.bAlterToMagicItemPct; + it.specular = t.bSpecular; + it.gain_socket_pct = t.bGainSocketPct; return it; } MobRecord parse_mob(const uint8_t *r, uint32_t stride) { MobRecord m; - if (!r || stride < 139) { + if (!r || stride != sizeof(SMobTable)) { return m; } - m.vnum = rd_u32(r + 0); - m.name = cstr(r + 4, 65); - m.locale_name = cstr(r + 69, 65); - m.type = r[134]; - m.rank = r[135]; - m.battle_type = r[136]; - m.level = r[137]; - m.size = r[138]; + SMobTable t; + std::memcpy(&t, r, sizeof(t)); + m.vnum = t.dwVnum; + m.name = cstr(t.szName, sizeof(t.szName)); + m.locale_name = cstr(t.szLocaleName, sizeof(t.szLocaleName)); + m.type = t.bType; + m.rank = t.bRank; + m.battle_type = t.bBattleType; + m.level = t.bLevel; + m.size = t.bSize; + m.attack_speed = t.sAttackSpeed; + m.moving_speed = t.sMovingSpeed; + m.on_click_type = t.bOnClickType; + m.empire = t.bEmpire; + m.folder = cstr(t.szFolder, sizeof(t.szFolder)); + m.monster_color = t.dwMonsterColor; return m; } diff --git a/extension/src/proto/proto.h b/extension/src/proto/proto.h index 245f35c5..90b5e306 100644 --- a/extension/src/proto/proto.h +++ b/extension/src/proto/proto.h @@ -1,15 +1,13 @@ #pragma once -// item_proto / mob_proto reader for this m2dev fork. +// item_proto / mob_proto reader, the 40250 format (GameLib/ItemManager.cpp CItemManager::LoadItemTable, +// UserInterface/PythonNonPlayer.cpp CPythonNonPlayer::LoadNonPlayerData). // -// Outer: MIPX (item) = [u32 'MIPX'][u32 ver=1][u32 stride][u32 elements][u32 datasize][blob] -// MMPT (mob) = [u32 'MMPT'][u32 elements][u32 datasize][blob] -// Blob = CLZO container: [u32 'MCOZ'][u32 encryptSize][u32 compressedSize][u32 realSize] -// data @ blob+20. encryptSize>0 -> XChaCha20-decrypt (the fork's tea_*: key = -// BLAKE2b(key16,"M2DevPackEncrypt"), nonce = BLAKE2b(key16,"M2DevNonce")[:24]) of -// encryptSize bytes starting at blob+16, yielding [u32 'MCOZ'][lzo1x] -> LZO -> -// realSize bytes. encryptSize==0 -> LZO straight from blob+20. -// Decompressed blob = `elements` records of `stride` bytes (stride from the MIPX header, -// or realSize/elements for MMPT). Records are #pragma pack(1). +// Outer: item = [u32 'MIPX'][u32 ver=1][u32 stride=156][u32 elements][u32 datasize][CLZObject] +// (or the older [u32 'MIPT'][u32 elements][u32 datasize][CLZObject]) +// mob = [u32 'MMPT'][u32 elements][u32 datasize][CLZObject] +// CLZObject = [u32 'MCOZ'][u32 encryptSize][u32 compressedSize][u32 realSize] + TEA/LZO payload, decoded by the +// ported CLZO::Decompress with the table's key. Records are #pragma pack(1) ILP32 structs: +// CItemData::TItemTable (156 bytes, port/GameLib/ItemData.h) and CPythonNonPlayer::TMobTable (255 bytes). #include #include @@ -18,14 +16,15 @@ namespace mtproto { -// The 4-DWORD keys the client hardcodes (GameLib/ItemManager.cpp, PythonNonPlayer.cpp). +// The 4-DWORD keys 40250 hardcodes: s_adwItemProtoKey (GameLib/ItemManager.cpp), s_adwMobProtoKey +// (UserInterface/PythonNonPlayer.cpp). inline constexpr std::array ITEM_PROTO_KEY = {173217u, 72619434u, 408587239u, 27973291u}; inline constexpr std::array MOB_PROTO_KEY = {4813894u, 18955u, 552631u, 6822045u}; struct Proto { uint32_t fourcc = 0; uint32_t version = 0; - uint32_t stride = 0; // record size + uint32_t stride = 0; // record size: sizeof(TItemTable) or sizeof(TMobTable) uint32_t elements = 0; // record count std::vector blob; // elements * stride bytes @@ -34,14 +33,14 @@ struct Proto { } }; -// Load + decompress. `key` is ITEM_PROTO_KEY or MOB_PROTO_KEY. -// Parse from an already-read buffer (host reads via godot::FileAccess for res://). +// Load + decompress. `key` is ITEM_PROTO_KEY or MOB_PROTO_KEY; the format follows the fourcc. +// Parse from an already-read buffer (the extension reads through asset_io, e.g. pack://locale/en/item_proto). bool load_proto_bytes(const std::vector &bytes, const std::array &key, Proto &out, std::string *err); bool load_proto(const std::string &path, const std::array &key, Proto &out, std::string *err = nullptr); -// --- typed views over the leading fields (rest is offset-stable per stride) --- +// --- typed views over the records --- struct ItemLimit { uint8_t type = 0; @@ -53,42 +52,53 @@ struct ItemApply { int32_t value = 0; }; +// CItemData::TItemTable, field for field. struct ItemRecord { uint32_t vnum = 0; uint32_t vnum_range = 0; - std::string name; // szName[65] @ 8 - std::string locale_name; // szLocaleName[65] @ 73 - uint8_t type = 0; // @ 138 - uint8_t sub_type = 0; // @ 139 - uint8_t weight = 0; // @ 140 - uint8_t size = 0; // @ 141 - uint32_t anti_flags = 0; // @ 142 (ITEM_ANTIFLAG_*; sell = 1 << 8) - uint32_t flags = 0; // @ 146 (ITEM_FLAG_*; count-per-1-gold = 1 << 3) - uint32_t wear_flags = 0; // @ 150 - uint32_t buy_price = 0; // @ 158 - uint32_t sell_price = 0; // @ 162 - std::array limits{}; // aLimits[2] @ 166 (type + long) - std::array applies{}; // aApplies[3] @ 176 (type + long) - int32_t values[6] = {0}; // alValues[6] @ 191 (armor: values[3] = body shape index) - int32_t sockets[3] = {0}; // alSockets[3] @ 215 - uint32_t refined_vnum = 0; // dwRefinedVnum @ 227 - uint16_t refine_set = 0; // wRefineSet @ 231 - uint8_t alter_to_magic_pct = 0; // bAlterToMagicItemPct @ 233 - uint8_t specular = 0; // @ 234 -> PARITY §2.7 fSpecular = specular/100 - uint8_t gain_socket_pct = 0; // bGainSocketPct @ 235 + std::string name; // szName[25] + std::string locale_name; // szLocaleName[25] + uint8_t type = 0; + uint8_t sub_type = 0; + uint8_t weight = 0; + uint8_t size = 0; + uint32_t anti_flags = 0; // ITEM_ANTIFLAG_* + uint32_t flags = 0; // ITEM_FLAG_* + uint32_t wear_flags = 0; + uint32_t immune_flag = 0; + uint32_t buy_price = 0; // dwIBuyItemPrice + uint32_t sell_price = 0; // dwISellItemPrice + std::array limits{}; + std::array applies{}; + int32_t values[6] = {0}; // alValues (armor: values[3] = body shape index) + int32_t sockets[3] = {0}; + uint32_t refined_vnum = 0; + uint16_t refine_set = 0; + uint8_t alter_to_magic_pct = 0; + uint8_t specular = 0; // PARITY §2.7 fSpecular = specular/100 + uint8_t gain_socket_pct = 0; }; +// Empty record when `stride` is not sizeof(TItemTable). ItemRecord parse_item(const uint8_t *rec, uint32_t stride); +// The leading CPythonNonPlayer::TMobTable fields plus the ones the client reads for display. struct MobRecord { uint32_t vnum = 0; - std::string name; // szName[65] @ 4 - std::string locale_name; // @ 69 - uint8_t type = 0; // @ 134 - uint8_t rank = 0; // @ 135 - uint8_t battle_type = 0; // @ 136 - uint8_t level = 0; // @ 137 - uint8_t size = 0; // @ 138 + std::string name; // szName[25] + std::string locale_name; // szLocaleName[25] + uint8_t type = 0; + uint8_t rank = 0; + uint8_t battle_type = 0; + uint8_t level = 0; + uint8_t size = 0; + int16_t attack_speed = 0; + int16_t moving_speed = 0; + uint8_t on_click_type = 0; + uint8_t empire = 0; + std::string folder; // szFolder[65] + uint32_t monster_color = 0; }; +// Empty record when `stride` is not sizeof(TMobTable). MobRecord parse_mob(const uint8_t *rec, uint32_t stride); } // namespace mtproto diff --git a/extension/src/proto/proto_node.cpp b/extension/src/proto/proto_node.cpp index 48bcd570..69f6f239 100644 --- a/extension/src/proto/proto_node.cpp +++ b/extension/src/proto/proto_node.cpp @@ -10,6 +10,24 @@ using namespace godot; namespace mtgodot { +namespace { + +// szName is the Korean (CP949) development name; what 40250 shows is szLocaleName (CItemData::GetName, +// CPythonNonPlayer::GetMonsterName) in the locale code page, 1252 for locale.cfg "10002 1252 en". +String from_cp1252(const std::string &s) { + static const char32_t k80[32] = { + 0x20AC, 0xFFFD, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0xFFFD, 0x017D, 0xFFFD, + 0xFFFD, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0xFFFD, 0x017E, 0x0178, + }; + String out; + for (unsigned char c : s) { + out += String::chr(c >= 0x80 && c < 0xA0 ? k80[c - 0x80] : static_cast(c)); + } + return out; +} + +} // namespace + void Metin2Proto::_bind_methods() { ClassDB::bind_method(D_METHOD("load_item_proto", "path"), &Metin2Proto::load_item_proto); ClassDB::bind_method(D_METHOD("load_mob_proto", "path"), &Metin2Proto::load_mob_proto); @@ -65,8 +83,8 @@ Dictionary Metin2Proto::item(int vnum) const { mtproto::ItemRecord r = mtproto::parse_item(m_item.record(it->second), m_item.stride); d["vnum"] = (int)r.vnum; d["vnum_range"] = (int)r.vnum_range; - d["name"] = String::utf8(r.name.c_str()); - d["locale_name"] = String::utf8(r.locale_name.c_str()); + d["name"] = from_cp1252(r.locale_name); // CItemData::GetName() + d["locale_name"] = from_cp1252(r.locale_name); d["type"] = (int)r.type; d["sub_type"] = (int)r.sub_type; d["weight"] = (int)r.weight; @@ -74,6 +92,7 @@ Dictionary Metin2Proto::item(int vnum) const { d["anti_flags"] = (int)r.anti_flags; d["flags"] = (int)r.flags; d["wear_flags"] = (int)r.wear_flags; + d["immune_flag"] = (int)r.immune_flag; d["buy_price"] = (int)r.buy_price; d["sell_price"] = (int)r.sell_price; { @@ -126,13 +145,19 @@ Dictionary Metin2Proto::mob(int vnum) const { } mtproto::MobRecord r = mtproto::parse_mob(m_mob.record(it->second), m_mob.stride); d["vnum"] = (int)r.vnum; - d["name"] = String::utf8(r.name.c_str()); - d["locale_name"] = String::utf8(r.locale_name.c_str()); + d["name"] = from_cp1252(r.locale_name); // CPythonNonPlayer::GetMonsterName() + d["locale_name"] = from_cp1252(r.locale_name); d["type"] = (int)r.type; d["rank"] = (int)r.rank; d["battle_type"] = (int)r.battle_type; d["level"] = (int)r.level; d["size"] = (int)r.size; + d["attack_speed"] = (int)r.attack_speed; + d["moving_speed"] = (int)r.moving_speed; + d["on_click_type"] = (int)r.on_click_type; + d["empire"] = (int)r.empire; + d["folder"] = String(r.folder.c_str()); + d["monster_color"] = (int64_t)r.monster_color; return d; } diff --git a/extension/src/proto/proto_node.h b/extension/src/proto/proto_node.h index a7d5d4b1..8f25dd9d 100644 --- a/extension/src/proto/proto_node.h +++ b/extension/src/proto/proto_node.h @@ -2,7 +2,7 @@ // Metin2Proto — GDExtension node exposing item_proto / mob_proto to GDScript. // // var proto = Metin2Proto.new() -// proto.load_item_proto("/locale/locale/en/item_proto") +// proto.load_item_proto("pack://locale/en/item_proto") # 40250 format, via asset_io // var d := proto.item(19) # { vnum, name, locale_name, type, sub_type, ... } // // Used by the P2 inventory/equipment windows for names / types / tooltips and by diff --git a/extension/tests/proto_item_layout_test.cpp b/extension/tests/proto_item_layout_test.cpp index fa7ab064..71f500c1 100644 --- a/extension/tests/proto_item_layout_test.cpp +++ b/extension/tests/proto_item_layout_test.cpp @@ -1,5 +1,5 @@ -// Synthetic item_proto record test. It locks the offsets used by the -// ClientVS22 TItemTable layout without requiring the 2.2G asset tree. +// Synthetic item_proto / mob_proto records. Locks the 40250 CItemData::TItemTable (156 bytes) and +// CPythonNonPlayer::TMobTable (255 bytes) offsets without the client assets. #include "../src/proto/proto.h" #include @@ -16,51 +16,59 @@ static int g_fail = 0; } \ } while (0) -static void put_u32(std::array &record, size_t offset, uint32_t value) { +template +static void put_u32(std::array &record, size_t offset, uint32_t value) { record[offset + 0] = static_cast(value); record[offset + 1] = static_cast(value >> 8); record[offset + 2] = static_cast(value >> 16); record[offset + 3] = static_cast(value >> 24); } -int main() { - std::array record{}; +template +static void put_u16(std::array &record, size_t offset, uint16_t value) { + record[offset + 0] = static_cast(value); + record[offset + 1] = static_cast(value >> 8); +} + +static void item_layout() { + std::array record{}; put_u32(record, 0, 12345); put_u32(record, 4, 7); std::memcpy(record.data() + 8, "TestItem", 9); - std::memcpy(record.data() + 73, "LocaleItem", 11); - record[138] = 3; - record[139] = 4; - record[140] = 2; - record[141] = 1; - put_u32(record, 142, 1u << 8); - put_u32(record, 146, 1u << 3); - put_u32(record, 150, 0x12345678); - put_u32(record, 158, 9000); - put_u32(record, 162, 6000); - record[166] = 1; - put_u32(record, 167, 42); - record[176] = 2; - put_u32(record, 177, -7); - put_u32(record, 191 + 3 * 4, 3); - put_u32(record, 215, 1001); - put_u32(record, 219, -1); - put_u32(record, 227, 12346); - record[231] = 0x34; - record[232] = 0x12; - record[233] = 11; - record[234] = 75; - record[235] = 88; + std::memcpy(record.data() + 33, "LocaleItem", 11); + record[58] = 3; + record[59] = 4; + record[60] = 2; + record[61] = 1; + put_u32(record, 62, 1u << 8); + put_u32(record, 66, 1u << 3); + put_u32(record, 70, 0x12345678); + put_u32(record, 74, 0x55); + put_u32(record, 78, 9000); + put_u32(record, 82, 6000); + record[86] = 1; + put_u32(record, 87, 42); + record[96] = 2; + put_u32(record, 97, static_cast(-7)); + put_u32(record, 111 + 3 * 4, 3); + put_u32(record, 135, 1001); + put_u32(record, 139, static_cast(-1)); + put_u32(record, 147, 12346); + put_u16(record, 151, 0x1234); + record[153] = 11; + record[154] = 75; + record[155] = 88; const mtproto::ItemRecord item = mtproto::parse_item(record.data(), record.size()); CHECK(item.vnum == 12345, "vnum offset"); CHECK(item.vnum_range == 7, "vnum range offset"); CHECK(item.name == "TestItem", "name offset"); CHECK(item.locale_name == "LocaleItem", "locale name offset"); - CHECK(item.type == 3 && item.sub_type == 4, "type offsets"); + CHECK(item.type == 3 && item.sub_type == 4 && item.weight == 2 && item.size == 1, "type offsets"); CHECK(item.anti_flags == (1u << 8), "anti_flags offset"); CHECK(item.flags == (1u << 3), "flags offset"); CHECK(item.wear_flags == 0x12345678, "wear_flags offset"); + CHECK(item.immune_flag == 0x55, "immune_flag offset"); CHECK(item.buy_price == 9000 && item.sell_price == 6000, "price offsets"); CHECK(item.limits[0].type == 1 && item.limits[0].value == 42, "limit offsets"); CHECK(item.applies[0].type == 2 && item.applies[0].value == -7, "apply offsets"); @@ -69,11 +77,50 @@ int main() { CHECK(item.refined_vnum == 12346 && item.refine_set == 0x1234, "refine offsets"); CHECK(item.alter_to_magic_pct == 11 && item.specular == 75 && item.gain_socket_pct == 88, "tail offsets"); + CHECK(mtproto::parse_item(record.data(), 236).vnum == 0, "m2dev 236-byte stride rejected"); +} +static void mob_layout() { + std::array record{}; + put_u32(record, 0, 101); + std::memcpy(record.data() + 4, "Wild Dog", 9); + std::memcpy(record.data() + 29, "LocaleDog", 10); + record[54] = 0; // bType + record[55] = 1; // bRank + record[56] = 2; // bBattleType + record[57] = 45; // bLevel + record[58] = 1; // bSize + // 59 gold/exp/hp (16), 75 regen (2), 77 wDef, 79 AI/race/immune (12), 91 str..int (4), 95 damage (8) + put_u16(record, 103, static_cast(-5)); // sAttackSpeed + put_u16(record, 105, 120); // sMovingSpeed + // 107 aggr pct, 108 sight, 110 range, 112 enchants[6], 118 resists[11], 129 resurrection, 133 drop + record[137] = 3; // bMountCapacity + record[138] = 1; // bOnClickType + record[139] = 2; // bEmpire + std::memcpy(record.data() + 140, "monster/dog", 12); + // 205 fDamMultiply, 209 summon, 213 drain sp + put_u32(record, 217, 0xAABBCCDD); // dwMonsterColor + // 221 polymorph, 225 skills 5x5, 250 berserk..revive + record[254] = 9; + + const mtproto::MobRecord m = mtproto::parse_mob(record.data(), record.size()); + CHECK(m.vnum == 101, "mob vnum offset"); + CHECK(m.name == "Wild Dog" && m.locale_name == "LocaleDog", "mob name offsets"); + CHECK(m.rank == 1 && m.battle_type == 2 && m.level == 45 && m.size == 1, "mob byte offsets"); + CHECK(m.attack_speed == -5 && m.moving_speed == 120, "mob speed offsets"); + CHECK(m.on_click_type == 1 && m.empire == 2, "mob click/empire offsets"); + CHECK(m.folder == "monster/dog", "mob folder offset"); + CHECK(m.monster_color == 0xAABBCCDD, "mob color offset"); + CHECK(mtproto::parse_mob(record.data(), 335).vnum == 0, "m2dev 335-byte stride rejected"); +} + +int main() { + item_layout(); + mob_layout(); if (g_fail) { std::fprintf(stderr, "%d check(s) failed\n", g_fail); return 1; } - std::printf("all item layout checks passed\n"); + std::printf("all item/mob layout checks passed\n"); return 0; } diff --git a/extension/tests/proto_test.cpp b/extension/tests/proto_test.cpp index 73580611..1e53b59c 100644 --- a/extension/tests/proto_test.cpp +++ b/extension/tests/proto_test.cpp @@ -1,7 +1,12 @@ -// item_proto / mob_proto reader — against the real locale files if present. -// Set M2_ASSETS to the Metin2 assets dir, else falls back to a repo-relative -// guess; skips (passes) if the files are not found. +// item_proto / mob_proto against the real 40250 client: registers /pack like the client does, reads +// "locale/en/item_proto" and "locale/en/mob_proto" through CEterPackManager::Get (the path +// CPythonApplication::Create hands LoadItemTable/LoadNonPlayerData), and parses them in the 40250 format. +// +// proto_test <40250 Client dir> +// +// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which turns that into a failure. #include "../src/proto/proto.h" +#include "../src/platform/PackBackend.h" #include #include @@ -18,46 +23,43 @@ static int g_fail = 0; } \ } while (0) -static std::string assets_root() { - if (const char *e = std::getenv("M2_ASSETS")) { - return e; +static bool load(const char *name, const std::array &key, Proto &p) { + std::vector bytes; + if (!mtpack40250::read(name, bytes)) { + std::fprintf(stderr, "FAIL: %s not in the packs\n", name); + ++g_fail; + return false; } - return "../../assets"; // ctest cwd = build/extension -> /assets + std::string err; + const bool ok = load_proto_bytes(bytes, key, p, &err); + CHECK(ok, (std::string("load ") + name + ": " + err).c_str()); + return ok; } -static bool exists(const std::string &p) { - FILE *f = std::fopen(p.c_str(), "rb"); - if (f) { - std::fclose(f); - return true; +int main(int argc, char **argv) { + const char *strict = std::getenv("MT_ASSETS_STRICT"); + const bool is_strict = strict && std::string(strict) == "1"; + const std::string client = argc > 1 ? argv[1] : ""; + if (client.empty() || !mtpack40250::initialize(client)) { + std::printf("proto_test: no 40250 Client/pack at '%s'\n", client.c_str()); + return is_strict ? 1 : 77; } - return false; -} -int main() { - const std::string base = assets_root() + "/locale/locale/en"; - const std::string ipath = base + "/item_proto"; - const std::string mpath = base + "/mob_proto"; - - if (!exists(ipath) || !exists(mpath)) { - std::printf("skipped (no locale proto files at %s)\n", base.c_str()); - return 0; - } + // CEterPackManager::isExist, what AssetRoot.locale_file() asks before loading. + CHECK(mtpack40250::exists("locale/en/item_proto"), "isExist locale/en/item_proto"); + CHECK(mtpack40250::exists("LOCALE\\EN\\MOB_PROTO"), "isExist normalizes case and separators"); + CHECK(!mtpack40250::exists("locale/en/no_such_proto"), "isExist rejects a missing name"); // --- item_proto --- { Proto p; - std::string err; - bool ok = load_proto(ipath, ITEM_PROTO_KEY, p, &err); - CHECK(ok, ("load item_proto: " + err).c_str()); - if (ok) { + if (load("locale/en/item_proto", ITEM_PROTO_KEY, p)) { CHECK(p.fourcc == 0x5850494Du, "item fourcc MIPX"); CHECK(p.version == 1, "item version 1"); - CHECK(p.stride == 236, "item stride 236 (== pack(1) sizeof TItemTable)"); + CHECK(p.stride == 156, "item stride 156 (== sizeof TItemTable)"); CHECK(p.elements > 100, "item elements > 100"); CHECK(p.blob.size() == static_cast(p.stride) * p.elements, "item blob size"); - // every record: vnum monotonically increasing, name printable ascii uint32_t prev = 0; int named = 0, mono = 1; uint8_t max_spec = 0; @@ -74,68 +76,61 @@ int main() { max_spec = it.specular; } } - CHECK(mono, "item vnums non-decreasing"); + CHECK(mono, "item vnums increasing"); CHECK(named > p.elements * 0.8, "item: >80% have a name"); std::printf("item_proto: %u items, stride %u, %d named, max bSpecular=%u\n", p.elements, p.stride, named, max_spec); - ItemRecord first = parse_item(p.record(0), p.stride); - std::printf(" [0] vnum=%u '%s' type=%u sub=%u weight=%u\n", first.vnum, - first.name.c_str(), first.type, first.sub_type, first.weight); - CHECK(first.vnum > 0 && first.vnum < 100000, "item[0] vnum sane"); - CHECK(!first.name.empty(), "item[0] has a name"); - - // alValues[3] = body-armor shape index (anchored by bSpecular @234). - // "Monk Plate Armour" 11200..11209 all share shape 3; specular ramps - // with the refine level (0 -> 100). + // alValues[3] = body-armor shape index; "Monk Plate Armour" 11200..11209 share shape 3 and + // bSpecular ramps with the refine level. bool found_armor = false; for (uint32_t i = 0; i < p.elements; ++i) { ItemRecord it = parse_item(p.record(i), p.stride); if (it.vnum == 11209) { found_armor = true; - std::printf(" 11209 values=[%d,%d,%d,%d,%d,%d] spec=%u\n", it.values[0], - it.values[1], it.values[2], it.values[3], it.values[4], it.values[5], - it.specular); + std::printf(" 11209 '%s' values=[%d,%d,%d,%d,%d,%d] spec=%u\n", it.locale_name.c_str(), + it.values[0], it.values[1], it.values[2], it.values[3], it.values[4], + it.values[5], it.specular); CHECK(it.values[3] == 3, "item 11209: values[3] == shape 3"); CHECK(it.specular == 100, "item 11209 (+9): bSpecular == 100"); } if (it.vnum == 11200) { - CHECK(it.values[3] == 3 && it.specular == 0, - "item 11200 (+0): shape 3, specular 0"); + CHECK(it.values[3] == 3 && it.specular == 0, "item 11200 (+0): shape 3, specular 0"); } } CHECK(found_armor, "item_proto contains vnum 11209"); + // CItemData::GetName() is szLocaleName. + ItemRecord yang = parse_item(p.record(0), p.stride); + std::printf(" [0] vnum=%u locale_name='%s'\n", yang.vnum, yang.locale_name.c_str()); + CHECK(yang.vnum == 1 && yang.locale_name == "Yang", "item[0] = vnum 1 'Yang'"); } } // --- mob_proto --- { Proto p; - std::string err; - bool ok = load_proto(mpath, MOB_PROTO_KEY, p, &err); - CHECK(ok, ("load mob_proto: " + err).c_str()); - if (ok) { + if (load("locale/en/mob_proto", MOB_PROTO_KEY, p)) { CHECK(p.fourcc == 0x54504D4Du, "mob fourcc MMPT"); - CHECK(p.stride == 335, "mob stride 335 (derived from realSize/elements)"); + CHECK(p.stride == 255, "mob stride 255 (== sizeof TMobTable)"); CHECK(p.elements > 100, "mob elements > 100"); - int named = 0, maxlvl = 0; + int named = 0, maxlvl = 0, with_folder = 0; for (uint32_t i = 0; i < p.elements; ++i) { MobRecord m = parse_mob(p.record(i), p.stride); - if (!m.name.empty()) { - ++named; - } + named += !m.name.empty(); + with_folder += !m.folder.empty(); if (m.level > maxlvl) { maxlvl = m.level; } } CHECK(named > p.elements * 0.8, "mob: >80% named"); + CHECK(with_folder > p.elements * 0.5, "mob: >50% have a model folder"); CHECK(maxlvl > 20 && maxlvl < 256, "mob levels in a sane range"); MobRecord first = parse_mob(p.record(0), p.stride); std::printf("mob_proto: %u mobs, stride %u, %d named, maxlvl=%d\n [0] vnum=%u '%s' " - "type=%u rank=%u level=%u\n", - p.elements, p.stride, named, maxlvl, first.vnum, first.name.c_str(), first.type, - first.rank, first.level); + "type=%u rank=%u level=%u folder='%s'\n", + p.elements, p.stride, named, maxlvl, first.vnum, first.locale_name.c_str(), first.type, + first.rank, first.level, first.folder.c_str()); CHECK(first.vnum > 0 && first.vnum < 60000, "mob[0] vnum sane"); CHECK(!first.name.empty(), "mob[0] has a name"); } diff --git a/project/asset_root.gd b/project/asset_root.gd index 0bbaa806..f7d55eca 100644 --- a/project/asset_root.gd +++ b/project/asset_root.gd @@ -45,3 +45,28 @@ static func sub(rel: String) -> String: # 资源目录是否真的存在(缺资源的测试据此跳过)。 static func available() -> bool: return DirAccess.dir_exists_absolute(path()) + +# --- 40250 client (pack/Index + .eix/.epk), docs/PORT-PLAN.md 2R item 6 / 2D --- +# 解析顺序:环境变量 MT_40250_CLIENT;开发树 res://../../40250/Server Client TMP4/Client。 +static func client_path() -> String: + var env := OS.get_environment("MT_40250_CLIENT") + if env != "": + return env + return ProjectSettings.globalize_path("res://../../40250/Server Client TMP4/Client").simplify_path() + +# 按 40250 PackInitialize("pack") 注册 /pack;只注册一次,之后返回首次结果。 +static func pack_ready() -> bool: + if not ClassDB.class_exists("Metin2Pack"): + return false + return bool(ClassDB.class_call_static("Metin2Pack", "initialize", client_path())) + +# 40250 的 locale 文件(CPythonApplication::Create: "%s/item_proto" % LocalePath,LocalePath = "locale/"), +# 走 pack://;该语言没有时回退 en。pack 没就绪返回 ""。 +static func locale_file(name: String, lang: String = "en") -> String: + if not pack_ready(): + return "" + for l in [lang, "en"]: + var p := "pack://locale/%s/%s" % [l, name] + if bool(ClassDB.class_call_static("Metin2Pack", "exists", "locale/%s/%s" % [l, name])): + return p + return "" diff --git a/project/equip_model_test.gd b/project/equip_model_test.gd index 923df90d..240b79c6 100644 --- a/project/equip_model_test.gd +++ b/project/equip_model_test.gd @@ -278,8 +278,8 @@ func _run() -> void: if ClassDB.class_exists("Metin2Proto"): var pr: Object = ClassDB.instantiate("Metin2Proto") get_root().add_child(pr) - var ip := assets.path_join("locale/locale/en/item_proto") - if FileAccess.file_exists(ip) and pr.call("load_item_proto", ip): + var ip := AssetRoot.locale_file("item_proto") + if ip != "" and pr.call("load_item_proto", ip): var em3: Node = EquipModel.new() get_root().add_child(em3) em3.setup(fc, il, func() -> Node: return model, assets) diff --git a/project/game_scene.gd b/project/game_scene.gd index c929bccd..0b1b9e5f 100644 --- a/project/game_scene.gd +++ b/project/game_scene.gd @@ -488,10 +488,8 @@ func setup(m2client: Node, assets_root: String, if ClassDB.class_exists("Metin2Proto") and assets_root != "": proto = ClassDB.instantiate("Metin2Proto") add_child(proto) - var ip := assets_root.path_join("locale/locale/%s/item_proto" % cur_lang) - if not FileAccess.file_exists(ip): - ip = assets_root.path_join("locale/locale/en/item_proto") - if FileAccess.file_exists(ip): + var ip := AssetRoot.locale_file("item_proto", cur_lang) + if ip != "": proto.call("load_item_proto", ip) # CPythonPlayer::IsEquipItemInSlot and SetItemData both consult the # same ItemManager table. Share this authority with native senders so @@ -505,10 +503,8 @@ func setup(m2client: Node, assets_root: String, chat.set_proto(proto) if quest_dialog and quest_dialog.has_method("set_proto"): quest_dialog.set_proto(proto) - var mp := assets_root.path_join("locale/locale/%s/mob_proto" % cur_lang) - if not FileAccess.file_exists(mp): - mp = assets_root.path_join("locale/locale/en/mob_proto") - if FileAccess.file_exists(mp): + var mp := AssetRoot.locale_file("mob_proto", cur_lang) + if mp != "": proto.call("load_mob_proto", mp) await _yield() net_play.proto = proto # 按 race 分类 NPC/怪(bType 在本 fork 不可靠) diff --git a/project/mob_view_test.gd b/project/mob_view_test.gd index 3b520811..45e93f59 100644 --- a/project/mob_view_test.gd +++ b/project/mob_view_test.gd @@ -175,8 +175,8 @@ func _run() -> void: var audio: Node = Audio.new() get_root().add_child(audio) audio.setup(assets) - var mp := assets.path_join("locale/locale/en/mob_proto") - if not FileAccess.file_exists(mp) or not proto.call("load_mob_proto", mp): + var mp := AssetRoot.locale_file("mob_proto") + if mp == "" or not proto.call("load_mob_proto", mp): print(" (skip: 无 mob_proto)") return diff --git a/project/netbridge_test.gd b/project/netbridge_test.gd index c7818f82..fe59030b 100644 --- a/project/netbridge_test.gd +++ b/project/netbridge_test.gd @@ -130,12 +130,12 @@ func _test_proto() -> void: return var p: Object = ClassDB.instantiate("Metin2Proto") get_root().add_child(p) - var root := AssetRoot.sub("locale/locale/en") - if not FileAccess.file_exists(root + "/item_proto"): - print(" (skip Metin2Proto load: no item_proto)") + var ip := AssetRoot.locale_file("item_proto") + if ip == "": + print(" (skip Metin2Proto load: no 40250 pack)") p.queue_free() return - _ck(p.call("load_item_proto", root + "/item_proto"), "load_item_proto: %s" % p.call("get_last_error")) + _ck(p.call("load_item_proto", ip), "load_item_proto: %s" % p.call("get_last_error")) _ck(p.call("item_count") > 1000, "item_count > 1000 (%d)" % p.call("item_count")) var yang: Dictionary = p.call("item", 1) _ck(yang.get("name", "") == "Yang", "item(1).name == Yang") diff --git a/project/remote_player_test.gd b/project/remote_player_test.gd index 4945ced1..1c3fbf64 100644 --- a/project/remote_player_test.gd +++ b/project/remote_player_test.gd @@ -46,8 +46,8 @@ func _run() -> void: if ClassDB.class_exists("Metin2Proto"): proto = ClassDB.instantiate("Metin2Proto") get_root().add_child(proto) - var proto_path := assets.path_join("locale/locale/en/item_proto") - if not FileAccess.file_exists(proto_path) or not proto.call("load_item_proto", proto_path): + var proto_path := AssetRoot.locale_file("item_proto") + if proto_path == "" or not proto.call("load_item_proto", proto_path): proto = null var effects := EffectRegistry.new() effects.setup(assets) diff --git a/project/ui/char_select_screen.gd b/project/ui/char_select_screen.gd index c806d8c2..58809b03 100644 --- a/project/ui/char_select_screen.gd +++ b/project/ui/char_select_screen.gd @@ -355,11 +355,9 @@ func _rebuild_model() -> void: if _proto == null and ClassDB.class_exists("Metin2Proto"): _proto = ClassDB.instantiate("Metin2Proto") add_child(_proto) - for lang in ["en", "common"]: - var ip := _assets.path_join("locale/locale/%s/item_proto" % lang) - if FileAccess.file_exists(ip): - _proto.call("load_item_proto", ip) - break + var ip := AssetRoot.locale_file("item_proto") + if ip != "": + _proto.call("load_item_proto", ip) var equip := EquipModel.new() pv.add_child(equip) equip.setup_remote(null, func() -> Node: return pv, _assets, job,