cleanup: remove Godot-era test data, formats/, dead proto reader and duplicates

- test/: bgfx/Godot render goldens and compare shots nothing reads any more;
  the Granny oracle result m2-numeric.json moves to oracle/
- formats/: only the .spt sniffer was still linked; it moves into
  platform/SpeedTreeLib, the rest and tools/map_probe go
- bgm/ duplicated Client/BGM byte for byte; tools/root archived root scripts
  that root.epk already holds; extension/src/proto had no build reference
- GEMINI.md duplicated AGENTS.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-29 18:43:19 +09:00
co-authored by Claude Opus 5.5
parent 0137f852d3
commit 70710477cf
198 changed files with 8 additions and 69264 deletions
+1 -2
View File
@@ -10,8 +10,7 @@ if(NOT TARGET mtpython)
list(FILTER MT_PLATFORM_SOURCES EXCLUDE REGEX "/ScriptLib/")
endif()
add_library(port_platform STATIC ${MT_PLATFORM_SOURCES})
target_link_libraries(port_platform PUBLIC port_logic PRIVATE mt3p::freetype xrender::libgr2 xrender::formats)
target_include_directories(port_platform PRIVATE ${CMAKE_SOURCE_DIR}/formats)
target_link_libraries(port_platform PUBLIC port_logic PRIVATE mt3p::freetype xrender::libgr2)
foreach(src IN LISTS MT_PLATFORM_SOURCES)
mt_port_apply_defines(${src})
endforeach()
@@ -21,7 +21,7 @@
#include "SpeedTreeLib/StdAfx.h"
#include <SpeedTreeRT.h>
#include <spt.h>
#include "spt.h"
#include "../EterLib/RenderCommands3D.h"
@@ -0,0 +1,93 @@
#include "spt.h"
#include <cctype>
#include <cstdint>
#include <cstring>
namespace fmt {
namespace {
bool ends_with_ci(const std::string& s, const char* suf) {
size_t n = std::strlen(suf);
if (s.size() < n) return false;
for (size_t i = 0; i < n; ++i)
if (std::tolower((unsigned char)s[s.size() - n + i]) != std::tolower((unsigned char)suf[i]))
return false;
return true;
}
std::string token_ci(const std::string& bytes, const char* needle) {
const size_t n = std::strlen(needle);
if (n == 0 || bytes.size() < n) return "";
for (size_t i = 0; i + n <= bytes.size(); ++i) {
bool match = true;
for (size_t j = 0; j < n; ++j) {
if (std::tolower((unsigned char)bytes[i + j]) !=
std::tolower((unsigned char)needle[j])) {
match = false;
break;
}
}
if (!match) continue;
size_t e = i + n;
while (e < bytes.size()) {
const unsigned char c = (unsigned char)bytes[e];
if (!(std::isalnum(c) || c == '_' || c == '-')) break;
++e;
}
return bytes.substr(i, e - i);
}
return "";
}
std::string as_dds(std::string s) {
if (s.empty()) return s;
const size_t dot = s.find_last_of('.');
if (dot != std::string::npos) s.resize(dot);
return s + ".dds";
}
} // namespace
bool sniff_spt(const std::string& b, SptInfo& out) {
out = SptInfo{};
if (b.size() < 20) return false;
std::memcpy(&out.header_dword, b.data(), 4);
uint32_t maglen = 0;
std::memcpy(&maglen, b.data() + 4, 4);
if (maglen >= 4 && maglen <= 32 && 8 + maglen <= b.size()) {
out.magic.assign(b.data() + 8, b.data() + 8 + maglen);
// 去掉可能的尾随 NUL
while (!out.magic.empty() && out.magic.back() == '\0') out.magic.pop_back();
}
out.ok = out.magic.rfind("__IdvSpt", 0) == 0;
// 扫描可打印 ASCII 段,收 .tga / .dds 结尾的
std::string cur;
auto flush = [&] {
if (cur.size() >= 5 && (ends_with_ci(cur, ".tga") || ends_with_ci(cur, ".dds"))) {
// 只留文件名基础部分之后的路径(原样)
out.texture_refs.push_back(cur);
}
cur.clear();
};
for (unsigned char c : b) {
if (c >= 0x20 && c < 0x7F) {
cur += (char)c;
} else {
flush();
}
}
flush();
// 这两个名字在真实 v2 .spt 中常紧跟二进制标记,不会由上面的
// "字符串必须正好以扩展名结尾" 规则捕获。
out.composite_texture = as_dds(token_ci(b, "CompositeMap"));
out.self_shadow_texture = as_dds(token_ci(b, "CompositeShadowMap"));
return out.ok;
}
} // namespace fmt
+34
View File
@@ -0,0 +1,34 @@
// .spt —— SpeedTree RT 私有格式(IDV Inc.)。
//
// W4 调研结论(2026-08-29):**几何无法读**。`.spt` 的解析在闭源 `CSpeedTreeRT::LoadTree()`
// 里(`m2dev-client-src-main/extern/library/SpeedTree/speedtree_static.lib`,实测 COFF x86-64,
// IDV 专有,不能链进 macOS/移动端、不可随意分发);`SpeedTreeLib/` 是渲染 wrapper,不含 parser。
// → 运行时使用读取真实 bark/composite atlas 的 proxy;真几何走经授权的 Windows x64 离线 exporter。
// 2026-09-11: hash-matched native exports for 87 local species are now staged in
// assets/TreeGeometry and loaded preferentially by the runtime. This sniffer
// remains only the fallback texture reader, not a geometry decoder.
//
// 这里只做「嗅探」:读文件头 magic + version,抽取内嵌贴图和 composite atlas token。
// 不解析枝干/叶片几何。无 godot 依赖。
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace fmt {
struct SptInfo {
bool ok = false;
std::string magic; // "__IdvSpt_02_" 等
uint32_t header_dword = 0; // magic 前的第一个 u32(版本/大小,未定)
std::vector<std::string> texture_refs; // 内嵌的 *.tga / *.dds 路径(原样)
// SpeedTree 2 把这两个 atlas 名写成不一定以 NUL 结尾的 token(例如
// "CompositeMapB1#N"),普通 printable-string 扫描会漏掉。统一转成 .dds,
// 供无几何 reader 的 proxy 渲染使用。
std::string composite_texture;
std::string self_shadow_texture;
};
bool sniff_spt(const std::string& bytes, SptInfo& out);
} // namespace fmt
-253
View File
@@ -1,253 +0,0 @@
#include "proto.h"
#include "EterBase/StdAfx.h"
#include "EterBase/lzo.h"
#include "GameLib/StdAfx.h"
#include "GameLib/ItemData.h"
#include "../platform/UserInterface/UserInterface.h"
#include <cstring>
#include <fstream>
namespace mtproto {
namespace {
// 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;
std::memcpy(&v, p, 4);
return v;
}
std::string cstr(const char *p, size_t maxn) {
size_t n = 0;
while (n < maxn && p[n]) {
++n;
}
return std::string(p, n);
}
} // namespace
bool load_proto(const std::string &path, const std::array<uint32_t, 4> &key, Proto &out,
std::string *err) {
std::ifstream f(path, std::ios::binary);
if (!f) {
if (err) *err = "cannot open " + path;
return false;
}
std::vector<uint8_t> file((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
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<uint8_t> &file, const std::array<uint32_t, 4> &key,
Proto &out, std::string *err) {
out = Proto{};
if (file.size() < 12) {
if (err) *err = "file too small";
return false;
}
size_t pos = 0;
auto read_u32 = [&](uint32_t &v) {
if (pos + 4 > file.size()) {
return false;
}
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 = "invalid proto type";
return false;
}
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;
}
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.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;
}
// 40250 walks dwElements records from the buffer without comparing against its size.
if (static_cast<uint64_t>(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<size_t>(out.stride) * out.elements);
return true;
}
ItemRecord parse_item(const uint8_t *r, uint32_t stride) {
ItemRecord it;
if (!r || stride != sizeof(TItemTable)) {
return it;
}
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 < CItemData::ITEM_APPLY_MAX_NUM; ++i) {
it.applies[i] = {t.aApplies[i].bType, t.aApplies[i].lValue};
}
for (int i = 0; i < CItemData::ITEM_VALUES_MAX_NUM; ++i) {
it.values[i] = t.alValues[i];
}
for (int i = 0; i < CItemData::ITEM_SOCKET_MAX_NUM; ++i) {
it.sockets[i] = t.alSockets[i];
}
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 != sizeof(SMobTable)) {
return m;
}
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;
}
} // namespace mtproto
-104
View File
@@ -1,104 +0,0 @@
#pragma once
// item_proto / mob_proto reader, the 40250 format (GameLib/ItemManager.cpp CItemManager::LoadItemTable,
// UserInterface/PythonNonPlayer.cpp CPythonNonPlayer::LoadNonPlayerData).
//
// 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 <array>
#include <cstdint>
#include <string>
#include <vector>
namespace mtproto {
// The 4-DWORD keys 40250 hardcodes: s_adwItemProtoKey (GameLib/ItemManager.cpp), s_adwMobProtoKey
// (UserInterface/PythonNonPlayer.cpp).
inline constexpr std::array<uint32_t, 4> ITEM_PROTO_KEY = {173217u, 72619434u, 408587239u, 27973291u};
inline constexpr std::array<uint32_t, 4> MOB_PROTO_KEY = {4813894u, 18955u, 552631u, 6822045u};
struct Proto {
uint32_t fourcc = 0;
uint32_t version = 0;
uint32_t stride = 0; // record size: sizeof(TItemTable) or sizeof(TMobTable)
uint32_t elements = 0; // record count
std::vector<uint8_t> blob; // elements * stride bytes
const uint8_t *record(uint32_t i) const {
return (i < elements) ? blob.data() + static_cast<size_t>(i) * stride : nullptr;
}
};
// 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<uint8_t> &bytes, const std::array<uint32_t, 4> &key,
Proto &out, std::string *err);
bool load_proto(const std::string &path, const std::array<uint32_t, 4> &key, Proto &out,
std::string *err = nullptr);
// --- typed views over the records ---
struct ItemLimit {
uint8_t type = 0;
int32_t value = 0;
};
struct ItemApply {
uint8_t type = 0;
int32_t value = 0;
};
// CItemData::TItemTable, field for field.
struct ItemRecord {
uint32_t vnum = 0;
uint32_t vnum_range = 0;
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<ItemLimit, 2> limits{};
std::array<ItemApply, 3> 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[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