# MotionRegistry —— 40250 playersettingmodule.py 的 RaceData motion 注册表适配。 # # 40250 不按文件名等权挑动作:RegisterCacheMotionData/SetMotionRandomWeight # 先把 (mode, index, sub-index, weight) 注册到 CRaceData,随后 # CActorInstance::GetRandomMotionKey 使用 random() % 100 和注册顺序选择。 # 这里读取随客户端资源发布的 playersettingmodule.py,保留相同的注册顺序、 # 重复项和权重;这样资源表才是唯一数据源,不能在 PlayerView 中复制一份常量。 extends RefCounted const MODE_VALUES := { "MOTION_MODE_GENERAL": 1, "MOTION_MODE_ONEHAND_SWORD": 2, "MOTION_MODE_TWOHAND_SWORD": 3, "MOTION_MODE_DUALHAND_SWORD": 4, "MOTION_MODE_BOW": 5, "MOTION_MODE_FAN": 6, "MOTION_MODE_BELL": 7, "MOTION_MODE_FISHING": 8, "MOTION_MODE_HORSE": 9, "MOTION_MODE_HORSE_ONEHAND_SWORD": 10, "MOTION_MODE_HORSE_TWOHAND_SWORD": 11, "MOTION_MODE_HORSE_DUALHAND_SWORD": 12, "MOTION_MODE_HORSE_BOW": 13, "MOTION_MODE_HORSE_FAN": 14, "MOTION_MODE_HORSE_BELL": 15, "MOTION_MODE_WEDDING_DRESS": 16, } # CRaceMotionData::NAME_* in GameLib/RaceMotionData.h. Only the indices used by # PlayerView are required here; unresolved skill/guild expressions are skipped, # exactly as an unregistered motion key is rejected by GetMotionKey. const MOTION_VALUES := { "MOTION_WAIT": 1, "MOTION_WALK": 2, "MOTION_RUN": 3, "MOTION_DAMAGE": 5, "MOTION_DAMAGE_FLYING": 6, "MOTION_STAND_UP": 7, "MOTION_DAMAGE_BACK": 8, "MOTION_DAMAGE_FLYING_BACK": 9, "MOTION_STAND_UP_BACK": 10, "MOTION_DEAD": 11, "MOTION_DIG": 12, "MOTION_NORMAL_ATTACK": 13, "MOTION_COMBO_ATTACK_1": 14, "MOTION_COMBO_ATTACK_2": 15, "MOTION_COMBO_ATTACK_3": 16, "MOTION_COMBO_ATTACK_4": 17, "MOTION_COMBO_ATTACK_5": 18, "MOTION_COMBO_ATTACK_6": 19, "MOTION_COMBO_ATTACK_7": 20, "MOTION_COMBO_ATTACK_8": 21, } const RACE_FUNCTIONS := [ "__LoadGameWarriorEx", "__LoadGameAssassinEx", "__LoadGameSuraEx", "__LoadGameShamanEx", ] var source_path := "" var source_loaded := false var _entries: Dictionary = {} static func load_for(assets_root: String, race: int) -> RefCounted: var result := new() result.load_from(assets_root, race) return result func load_from(assets_root: String, race: int) -> void: source_path = assets_root.path_join("root/playersettingmodule.py") _entries.clear() source_loaded = false if not FileAccess.file_exists(source_path): return var file := FileAccess.open(source_path, FileAccess.READ) if file == null: return var lines: Array[String] = [] while not file.eof_reached(): lines.append(file.get_line()) file.close() # SetGeneralMotions is a Python helper invoked by each PC race function. # Its `mode` parameter is the selected general mode in all four PC loaders. _parse_lines(_function_block(lines, "SetGeneralMotions"), 1) var function_name: String = RACE_FUNCTIONS[clampi(race & 3, 0, RACE_FUNCTIONS.size() - 1)] _parse_lines(_function_block(lines, function_name), -1) source_loaded = not _entries.is_empty() func variants(mode: int, index: int) -> Array: var value: Variant = _entries.get(_key(mode, index), []) return value.duplicate(true) if value is Array else [] func has_variants(mode: int, index: int) -> bool: var value: Variant = _entries.get(_key(mode, index), []) return value is Array and not value.is_empty() func has_mode(mode: int) -> bool: var prefix := "%d:" % mode for key in _entries.keys(): if String(key).begins_with(prefix): return true return false func _key(mode: int, index: int) -> String: return "%d:%d" % [mode, index] func _function_block(lines: Array[String], function_name: String) -> Array[String]: var result: Array[String] = [] var started := false var prefix := "def " + function_name + "(" for line in lines: if not started: if line.begins_with(prefix): started = true continue if line.begins_with("def "): break result.append(line) return result func _parse_lines(lines: Array[String], default_mode: int) -> void: for raw in lines: var line := raw var comment := line.find("#") if comment >= 0: line = line.substr(0, comment) line = line.strip_edges() if line.is_empty(): continue _parse_registration(line, default_mode) _parse_weight_update(line, default_mode) func _parse_registration(line: String, default_mode: int) -> void: var marker := "RegisterCacheMotionData(" var start := line.find(marker) if start < 0: return var close := line.rfind(")") if close <= start + marker.length(): return var args := line.substr(start + marker.length(), close - start - marker.length()).split(",") if args.size() < 3: return var mode := _resolve(args[0], default_mode, MODE_VALUES) var index := _resolve(args[1], -1, MOTION_VALUES) var filename := _literal_filename(args[2]) if mode < 0 or index < 0 or filename == "": return var weight := 100 if args.size() >= 4: weight = _integer_literal(args[3], 100) var key := _key(mode, index) var entries: Array = _entries.get(key, []) entries.append({"name": filename, "weight": weight}) _entries[key] = entries func _parse_weight_update(line: String, default_mode: int) -> void: var marker := "SetMotionRandomWeight(" var start := line.find(marker) if start < 0: return var close := line.rfind(")") if close <= start + marker.length(): return var args := line.substr(start + marker.length(), close - start - marker.length()).split(",") if args.size() < 4: return var mode := _resolve(args[0], default_mode, MODE_VALUES) var index := _resolve(args[1], -1, MOTION_VALUES) var sub_index := _integer_literal(args[2], -1) var weight := _integer_literal(args[3], -1) if mode < 0 or index < 0 or sub_index < 0 or weight < 0: return var key := _key(mode, index) var entries: Array = _entries.get(key, []) if sub_index >= entries.size(): return var entry: Dictionary = entries[sub_index] entry["weight"] = weight entries[sub_index] = entry _entries[key] = entries func _resolve(token: String, fallback: int, values: Dictionary) -> int: var text := token.strip_edges() if text == "mode": return fallback if fallback >= 0 else 1 if text.is_valid_int(): return int(text) var name := text.get_slice(".", text.get_slice_count(".") - 1) return int(values.get(name, fallback)) func _integer_literal(token: String, fallback: int) -> int: var text := token.strip_edges() return int(text) if text.is_valid_int() else fallback func _literal_filename(token: String) -> String: var text := token.strip_edges() if text.length() < 2: return "" if (text.begins_with("\"") and text.ends_with("\"")) or \ (text.begins_with("'") and text.ends_with("'")): return text.substr(1, text.length() - 2) return ""