完善客户端功能并同步差距文档
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
// M2 §3.5 —— playersettingmodule.py 的 ComboAttackNew 调用 -> PC 连击段表。
|
||||
#include "combo_table.h"
|
||||
#include "m2_tokvec.h" // fmt::read_file
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace fmt {
|
||||
|
||||
const std::vector<uint16_t>* ComboTable::get(uint16_t motion_mode, uint16_t combo_type) const {
|
||||
auto it = combos.find(make_combo_key(motion_mode, combo_type));
|
||||
return it == combos.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
const ComboTable& PlayerComboTables::klass(int combo_class) const {
|
||||
static const ComboTable kEmpty;
|
||||
if (combo_class < 0 || combo_class >= COMBO_CLASS_COUNT) return kEmpty;
|
||||
return per_class[combo_class];
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
size_t a = 0, b = s.size();
|
||||
while (a < b && std::isspace(static_cast<unsigned char>(s[a]))) ++a;
|
||||
while (b > a && std::isspace(static_cast<unsigned char>(s[b - 1]))) --b;
|
||||
return s.substr(a, b - a);
|
||||
}
|
||||
|
||||
bool starts_with(const std::string& s, const char* p) {
|
||||
return s.rfind(p, 0) == 0;
|
||||
}
|
||||
|
||||
// chr.MOTION_MODE_XXX -> CRaceMotionData::EMode 值。
|
||||
bool resolve_motion_mode(const std::string& suffix, long& out) {
|
||||
static const std::map<std::string, long> kMap = {
|
||||
{"RESERVED", 0}, {"GENERAL", 1}, {"ONEHAND_SWORD", 2}, {"TWOHAND_SWORD", 3},
|
||||
{"DUALHAND_SWORD", 4}, {"BOW", 5}, {"FAN", 6}, {"BELL", 7}, {"FISHING", 8},
|
||||
{"HORSE", 9}, {"HORSE_ONEHAND_SWORD", 10}, {"HORSE_TWOHAND_SWORD", 11},
|
||||
{"HORSE_DUALHAND_SWORD", 12}, {"HORSE_BOW", 13}, {"HORSE_FAN", 14},
|
||||
{"HORSE_BELL", 15}, {"WEDDING_DRESS", 16},
|
||||
};
|
||||
auto it = kMap.find(suffix);
|
||||
if (it == kMap.end()) return false;
|
||||
out = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
// COMBO_TYPE_N / COMBO_INDEX_N -> N-1(两者都是从 1 起的人读编号,值从 0 起)。
|
||||
bool resolve_one_based(const std::string& tail, long& out) {
|
||||
if (tail.empty()) return false;
|
||||
char* end = nullptr;
|
||||
long n = std::strtol(tail.c_str(), &end, 10);
|
||||
if (end == tail.c_str() || *end != '\0' || n < 1) return false;
|
||||
out = n - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 一个实参 token -> 整数。认符号常量,也认裸整数。
|
||||
bool resolve_token(const std::string& tok_in, long& out) {
|
||||
const std::string tok = trim(tok_in);
|
||||
if (tok.empty()) return false;
|
||||
|
||||
if (starts_with(tok, "chr.MOTION_MODE_")) {
|
||||
return resolve_motion_mode(tok.substr(sizeof("chr.MOTION_MODE_") - 1), out);
|
||||
}
|
||||
if (starts_with(tok, "chr.MOTION_COMBO_ATTACK_")) {
|
||||
if (!resolve_one_based(tok.substr(sizeof("chr.MOTION_COMBO_ATTACK_") - 1), out)) return false;
|
||||
out = COMBO_NAME_COMBO_ATTACK_1 + out; // 0-based n -> 段号 14 + n
|
||||
return true;
|
||||
}
|
||||
if (tok == "chr.MOTION_NORMAL_ATTACK") { out = COMBO_NAME_NORMAL_ATTACK; return true; }
|
||||
if (starts_with(tok, "COMBO_TYPE_")) { return resolve_one_based(tok.substr(sizeof("COMBO_TYPE_") - 1), out); }
|
||||
if (starts_with(tok, "COMBO_INDEX_")) { return resolve_one_based(tok.substr(sizeof("COMBO_INDEX_") - 1), out); }
|
||||
|
||||
// 裸整数
|
||||
char* end = nullptr;
|
||||
long n = std::strtol(tok.c_str(), &end, 10);
|
||||
if (end == tok.c_str() || *end != '\0') return false;
|
||||
out = n;
|
||||
return true;
|
||||
}
|
||||
|
||||
// "a, b, c)"(call 名后的部分)-> 解析出的整数实参。分号里没有嵌套括号。
|
||||
bool parse_call_args(const std::string& after_paren, std::vector<long>& out) {
|
||||
size_t close = after_paren.find(')');
|
||||
if (close == std::string::npos) return false;
|
||||
const std::string inner = after_paren.substr(0, close);
|
||||
out.clear();
|
||||
size_t start = 0;
|
||||
while (start <= inner.size()) {
|
||||
size_t comma = inner.find(',', start);
|
||||
std::string tok = inner.substr(start, comma == std::string::npos ? std::string::npos : comma - start);
|
||||
tok = trim(tok);
|
||||
if (!tok.empty()) {
|
||||
long v = 0;
|
||||
if (!resolve_token(tok, v)) return false;
|
||||
out.push_back(v);
|
||||
}
|
||||
if (comma == std::string::npos) break;
|
||||
start = comma + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int class_from_def_line(const std::string& line) {
|
||||
if (line.find("def __LoadGameWarriorEx") != std::string::npos) return COMBO_CLASS_WARRIOR;
|
||||
if (line.find("def __LoadGameAssassinEx") != std::string::npos) return COMBO_CLASS_ASSASSIN;
|
||||
if (line.find("def __LoadGameSuraEx") != std::string::npos) return COMBO_CLASS_SURA;
|
||||
if (line.find("def __LoadGameShamanEx") != std::string::npos) return COMBO_CLASS_SHAMAN;
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool parse_player_combo_tables(const std::string& py_text, PlayerComboTables& out, std::string* err) {
|
||||
out = PlayerComboTables{};
|
||||
|
||||
int cur_class = -1;
|
||||
size_t pos = 0;
|
||||
const size_t n = py_text.size();
|
||||
while (pos < n) {
|
||||
size_t eol = py_text.find('\n', pos);
|
||||
std::string line = py_text.substr(pos, eol == std::string::npos ? std::string::npos : eol - pos);
|
||||
pos = (eol == std::string::npos) ? n : eol + 1;
|
||||
|
||||
const std::string t = trim(line);
|
||||
if (t.empty() || t[0] == '#') continue;
|
||||
|
||||
// 顶层 def 换块:只有四个 __LoadGame<Class>Ex 有连击段调用,其它 def 复位。
|
||||
if (starts_with(line, "def ")) {
|
||||
cur_class = class_from_def_line(line);
|
||||
continue;
|
||||
}
|
||||
if (cur_class < 0) continue;
|
||||
|
||||
const char* kReserve = "chrmgr.ReserveComboAttackNew(";
|
||||
const char* kRegister = "chrmgr.RegisterComboAttackNew(";
|
||||
|
||||
size_t r = t.find(kReserve);
|
||||
if (r != std::string::npos) {
|
||||
std::vector<long> args;
|
||||
if (!parse_call_args(t.substr(r + std::string(kReserve).size()), args) || args.size() != 3) {
|
||||
if (err) *err = "combo_table: 无法解析 ReserveComboAttackNew: " + t;
|
||||
return false;
|
||||
}
|
||||
const uint32_t key = make_combo_key(static_cast<uint16_t>(args[0]), static_cast<uint16_t>(args[1]));
|
||||
const long count = args[2] < 0 ? 0 : args[2];
|
||||
// CRaceData::ReserveComboAttack 用 map::insert —— 已存在的 key 不覆盖。
|
||||
out.per_class[cur_class].combos.emplace(
|
||||
key, std::vector<uint16_t>(static_cast<size_t>(count), 0));
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t g = t.find(kRegister);
|
||||
if (g != std::string::npos) {
|
||||
std::vector<long> args;
|
||||
if (!parse_call_args(t.substr(g + std::string(kRegister).size()), args) || args.size() != 4) {
|
||||
if (err) *err = "combo_table: 无法解析 RegisterComboAttackNew: " + t;
|
||||
return false;
|
||||
}
|
||||
const uint32_t key = make_combo_key(static_cast<uint16_t>(args[0]), static_cast<uint16_t>(args[1]));
|
||||
const long idx = args[2];
|
||||
const uint16_t motion = static_cast<uint16_t>(args[3]);
|
||||
auto it = out.per_class[cur_class].combos.find(key);
|
||||
if (it == out.per_class[cur_class].combos.end()) continue; // 未 Reserve:跳过
|
||||
if (idx < 0 || static_cast<size_t>(idx) >= it->second.size()) continue; // 越界:跳过
|
||||
it->second[static_cast<size_t>(idx)] = motion;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (err) err->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parse_player_combo_tables_file(const std::string& path, PlayerComboTables& out, std::string* err) {
|
||||
std::string s;
|
||||
if (!read_file(path, s)) {
|
||||
if (err) *err = "combo_table: 打不开 " + path;
|
||||
return false;
|
||||
}
|
||||
return parse_player_combo_tables(s, out, err);
|
||||
}
|
||||
|
||||
} // namespace fmt
|
||||
Reference in New Issue
Block a user