143 lines
7.4 KiB
C++
143 lines
7.4 KiB
C++
// M2 §3.5 —— playersettingmodule.py 的 ComboAttackNew 调用 -> PC 连击段表。
|
||
// 照 GameLib/RaceData.cpp CRaceData::ReserveComboAttack / RegisterComboAttack 语义。
|
||
#include <combo_table.h>
|
||
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
#include <string>
|
||
|
||
using namespace fmt;
|
||
|
||
static int g_fail = 0;
|
||
#define CHECK(c, msg) \
|
||
do { \
|
||
if (!(c)) { \
|
||
std::fprintf(stderr, "FAIL: %s\n", (msg)); \
|
||
++g_fail; \
|
||
} \
|
||
} while (0)
|
||
|
||
// warrior_m/_w 共用的 __LoadGameWarriorEx 片段 + assassin 的 DUALHAND(男限),
|
||
// 外加一个非 Ex 的 def 用来验证块复位。缩进 / tab 混用照原文。
|
||
static const char *kPy = R"PY(
|
||
COMBO_TYPE_1 = 0
|
||
COMBO_TYPE_2 = 1
|
||
COMBO_TYPE_3 = 2
|
||
COMBO_INDEX_1 = 0
|
||
COMBO_INDEX_6 = 5
|
||
|
||
def __LoadGameWarriorEx(race, path):
|
||
chrmgr.SelectRace(race)
|
||
|
||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_GENERAL, COMBO_TYPE_1, 1)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_GENERAL, COMBO_TYPE_1, COMBO_INDEX_1, chr.MOTION_COMBO_ATTACK_1)
|
||
|
||
## Combo Type 1
|
||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, 4)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, COMBO_INDEX_1, chr.MOTION_COMBO_ATTACK_1)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, COMBO_INDEX_2, chr.MOTION_COMBO_ATTACK_2)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, COMBO_INDEX_3, chr.MOTION_COMBO_ATTACK_3)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_1, COMBO_INDEX_4, chr.MOTION_COMBO_ATTACK_4)
|
||
## Combo Type 2 —— 段号跳到 5 / 7(非连续,验证按下标写而非追加)
|
||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, 5)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_1, chr.MOTION_COMBO_ATTACK_1)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_2, chr.MOTION_COMBO_ATTACK_2)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_3, chr.MOTION_COMBO_ATTACK_3)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_4, chr.MOTION_COMBO_ATTACK_5)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, COMBO_INDEX_5, chr.MOTION_COMBO_ATTACK_7)
|
||
# 越界的 Register(下标 5 >= size 5):照参考端 AssertLog + return,忽略
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, 5, chr.MOTION_COMBO_ATTACK_8)
|
||
# 对已存在 key 的重复 Reserve:map::insert 不覆盖,原向量保留
|
||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_ONEHAND_SWORD, COMBO_TYPE_2, 99)
|
||
|
||
def __LoadGameAssassinEx(race, path):
|
||
chrmgr.SelectRace(race)
|
||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_DUALHAND_SWORD, COMBO_TYPE_3, 6)
|
||
chrmgr.RegisterComboAttackNew(chr.MOTION_MODE_DUALHAND_SWORD, COMBO_TYPE_3, COMBO_INDEX_6, chr.MOTION_COMBO_ATTACK_8)
|
||
|
||
def __LoadGameNPC():
|
||
# 块外的调用不该落到任何职业里
|
||
chrmgr.ReserveComboAttackNew(chr.MOTION_MODE_GENERAL, COMBO_TYPE_1, 3)
|
||
)PY";
|
||
|
||
static void check_vec(const std::vector<uint16_t> *v, std::initializer_list<uint16_t> want, const char *msg) {
|
||
if (!v) { std::fprintf(stderr, "FAIL: %s (nullptr)\n", msg); ++g_fail; return; }
|
||
if (v->size() != want.size()) { std::fprintf(stderr, "FAIL: %s (size %zu != %zu)\n", msg, v->size(), want.size()); ++g_fail; return; }
|
||
size_t i = 0;
|
||
for (uint16_t w : want) {
|
||
if ((*v)[i] != w) { std::fprintf(stderr, "FAIL: %s ([%zu]=%u != %u)\n", msg, i, (*v)[i], w); ++g_fail; return; }
|
||
++i;
|
||
}
|
||
}
|
||
|
||
int main() {
|
||
// ---- key 编码:RaceData.h MAKE_COMBO_KEY -------------------------------
|
||
CHECK(make_combo_key(COMBO_MODE_ONEHAND_SWORD, 1) == ((2u << 16) | 1u), "make_combo_key");
|
||
CHECK(combo_key_mode(make_combo_key(5, 2)) == 5 && combo_key_type(make_combo_key(5, 2)) == 2, "combo_key split");
|
||
|
||
// ---- 解析 ------------------------------------------------------------
|
||
PlayerComboTables tbl;
|
||
std::string err;
|
||
if (!parse_player_combo_tables(kPy, tbl, &err)) {
|
||
std::fprintf(stderr, "parse failed: %s\n", err.c_str());
|
||
return 1;
|
||
}
|
||
|
||
const ComboTable &w = tbl.klass(COMBO_CLASS_WARRIOR);
|
||
const ComboTable &a = tbl.klass(COMBO_CLASS_ASSASSIN);
|
||
const ComboTable &s = tbl.klass(COMBO_CLASS_SURA);
|
||
|
||
// GENERAL / type1 —— 单段普攻连击(NAME_COMBO_ATTACK_1 = 14)
|
||
check_vec(w.get(COMBO_MODE_GENERAL, 0), {14}, "warrior GENERAL type1 = [14]");
|
||
|
||
// ONEHAND_SWORD / type1 —— 连续 4 段 14..17
|
||
check_vec(w.get(COMBO_MODE_ONEHAND_SWORD, 0), {14, 15, 16, 17}, "warrior 1H type1 = [14,15,16,17]");
|
||
|
||
// ONEHAND_SWORD / type2 —— 5 段,末两段跳到 18 / 20(COMBO_ATTACK_5 / _7)
|
||
check_vec(w.get(COMBO_MODE_ONEHAND_SWORD, 1), {14, 15, 16, 18, 20}, "warrior 1H type2 = [14,15,16,18,20]");
|
||
|
||
// 越界 Register 被忽略;重复 Reserve 不覆盖(size 仍是 5,不是 99)
|
||
CHECK(w.get(COMBO_MODE_ONEHAND_SWORD, 1)->size() == 5, "dup Reserve ignored / OOB Register ignored");
|
||
|
||
// type3 从没 Reserve 过 -> GetComboDataPointer 应为 nullptr
|
||
CHECK(w.get(COMBO_MODE_ONEHAND_SWORD, 2) == nullptr, "warrior 1H type3 absent -> nullptr");
|
||
|
||
// 职业隔离:DUALHAND 只在 assassin 块里;warrior/sura 没有
|
||
check_vec(a.get(COMBO_MODE_DUALHAND_SWORD, 2), {0, 0, 0, 0, 0, 21}, "assassin DUAL type3 idx5 = COMBO_ATTACK_8(21)");
|
||
CHECK(w.get(COMBO_MODE_DUALHAND_SWORD, 2) == nullptr, "warrior has no DUALHAND table");
|
||
CHECK(s.empty(), "sura block absent in snippet -> empty table");
|
||
|
||
// 块外(__LoadGameNPC)的 Reserve 不该落进任何职业
|
||
size_t total = 0;
|
||
for (int c = 0; c < COMBO_CLASS_COUNT; ++c) total += tbl.klass(c).key_count();
|
||
CHECK(total == 4, "only the 4 in-Ex-block combo keys registered (2 warrior 1H + 1 warrior GENERAL + 1 assassin DUAL)");
|
||
|
||
// 越界职业索引 -> 空表,不崩
|
||
CHECK(tbl.klass(-1).empty() && tbl.klass(99).empty(), "out-of-range class -> empty");
|
||
|
||
// ---- 可选:真实 playersettingmodule.py(设了 M2_ASSETS 才跑)----------
|
||
if (const char *root = std::getenv("M2_ASSETS")) {
|
||
const std::string path = std::string(root) + "/root/playersettingmodule.py";
|
||
PlayerComboTables live;
|
||
std::string lerr;
|
||
if (parse_player_combo_tables_file(path, live, &lerr)) {
|
||
check_vec(live.klass(COMBO_CLASS_WARRIOR).get(COMBO_MODE_ONEHAND_SWORD, 0),
|
||
{14, 15, 16, 17}, "live warrior 1H type1");
|
||
// sura ONEHAND type3 末段回到 COMBO_ATTACK_4 (17)
|
||
const auto *sv = live.klass(COMBO_CLASS_SURA).get(COMBO_MODE_ONEHAND_SWORD, 2);
|
||
CHECK(sv && sv->size() == 6 && sv->back() == 17, "live sura 1H type3 tail = COMBO_ATTACK_4");
|
||
// shaman FAN type1 = 4 连续段
|
||
check_vec(live.klass(COMBO_CLASS_SHAMAN).get(COMBO_MODE_FAN, 0),
|
||
{14, 15, 16, 17}, "live shaman FAN type1");
|
||
std::printf("live playersettingmodule.py: OK\n");
|
||
} else {
|
||
std::fprintf(stderr, "live parse skipped: %s\n", lerr.c_str());
|
||
}
|
||
}
|
||
|
||
if (g_fail == 0) {
|
||
std::printf("PASS: formats_combo_table_test (§3.5 PC 连击段表)\n");
|
||
}
|
||
return g_fail == 0 ? 0 : 1;
|
||
}
|