# net_world_remote_action_test —— 40250 remote attack/combo action routing. # # The native client keeps FUNC_ATTACK and FUNC_COMBO separate all the way from # StateProcess to RunNormalAttack/RunComboAttack. This headless test checks # that the Godot presentation bridge preserves the weapon motion mode, the # combo segment (bArg), and the attack-speed ratio. extends SceneTree const NetWorld = preload("res://net_world.gd") class ActionView extends Node3D: var attack_calls: Array = [] var skill_calls: Array = [] var emotion_calls: Array = [] var sync_push_calls := 0 var states: Array[String] = [] signal motion_bound(state: String) func get_motion_mode() -> int: return 3 # CRaceMotionData::MODE_TWOHAND_SWORD func play_attack_motion(mode: int, index: int, speed: float) -> bool: attack_calls.append([mode, index, speed]) return true func set_anim_state(state: String) -> void: states.append(state) func play_skill_motion(name: String, grade: int, loop_count: int, moving_skill: bool) -> bool: skill_calls.append([name, grade, loop_count, moving_skill]) return true func set_motion_id(motion: int, target_race: int) -> bool: emotion_calls.append([motion, target_race]) return true func sync_push_motion() -> bool: sync_push_calls += 1 return true class SkillTableFake extends RefCounted: func motion_name_by_idx(idx: int) -> String: return "skill_7" if idx == 51 else "skill_%d" % idx func motion_grade_by_idx(idx: int) -> int: return 2 if idx == 51 else 0 class CollisionWorld extends Node: var samples := 0 func is_blocked(x: float, _z: float) -> bool: samples += 1 return x > 1.0 class GateClient extends Node: var calls: Array = [] var knockdown_calls: Array = [] var entities := {} func get_entity(vid: int) -> Dictionary: return entities.get(vid, {}) func get_entities() -> Array: return entities.values() func set_entity_network_state_gate(vid: int, skill_active: bool, skill_can_cancel: bool, acting_emotion: bool, tcp_state_enabled: bool) -> void: calls.append([vid, skill_active, skill_can_cancel, acting_emotion, tcp_state_enabled]) func set_entity_knockdown(vid: int, on: bool) -> void: knockdown_calls.append([vid, on]) var _fail := 0 func _ck(ok: bool, message: String) -> void: if not ok: _fail += 1 printerr("FAIL: " + message) func _init() -> void: await _run() if _fail == 0: print("PASS: net_world_remote_action_test") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: var nw: Node = NetWorld.new() get_root().add_child(nw) var view := ActionView.new() get_root().add_child(view) view.set_meta("attack_speed", 150) # Reference RunNormalAttack has no motion argument: it selects the normal # attack slot (13) in the active weapon motion mode. nw._apply_anim(view, NetWorld.FUNC_ATTACK, false, NetWorld.WALKMODE_RUN, false, 0) _ck(view.attack_calls.size() == 1, "FUNC_ATTACK reaches the attack-motion bridge") if view.attack_calls.size() >= 1: var normal: Array = view.attack_calls[0] _ck(int(normal[0]) == 3, "normal attack keeps the active weapon mode") _ck(int(normal[1]) == NetWorld.MOTION_NORMAL_ATTACK, "normal attack uses CRaceMotionData::NAME_NORMAL_ATTACK") _ck(is_equal_approx(float(normal[2]), 1.5), "attack speed 150 becomes fSpeedRatio 1.5") # Reference RunComboAttack passes the packet bArg through unchanged. nw._apply_anim(view, NetWorld.FUNC_COMBO, false, NetWorld.WALKMODE_RUN, false, 16) _ck(view.attack_calls.size() == 2, "FUNC_COMBO reaches the attack-motion bridge") if view.attack_calls.size() >= 2: var combo: Array = view.attack_calls[1] _ck(int(combo[0]) == 3, "combo keeps the active weapon mode") _ck(int(combo[1]) == 16, "combo preserves the packet motion index") _ck(is_equal_approx(float(combo[2]), 1.5), "combo keeps the remote attack speed") _ck(view.states.is_empty(), "successful attack-resource routing does not fall back to generic state") # The same bridge preserves the remote skill arg and releases the native # queue only from the one-shot motion tail; FUNC_EMOTION uses its uArg as the # CRaceMotionData action id. var gates := GateClient.new() get_root().add_child(gates) nw.client = gates nw.skill_table = SkillTableFake.new() view.set_meta("vid", 77) nw._by_vid[77] = view view.motion_bound.connect(nw._on_remote_motion_bound.bind(77)) nw._apply_anim(view, NetWorld.FUNC_SKILL | 7, false, NetWorld.WALKMODE_RUN, false, 0x12) _ck(view.skill_calls.size() == 1 and view.skill_calls[0] == ["skill_7", 0, 2, true], "FUNC_SKILL keeps loop-count and moving-skill bits") _ck(gates.calls.size() == 1 and gates.calls[0] == [77, true, false, false, true], "non-cancellable remote skill closes the TCP state gate") nw._apply_anim(view, NetWorld.FUNC_SKILL | 51, false, NetWorld.WALKMODE_RUN, false, 0) _ck(view.skill_calls.size() == 2 and view.skill_calls[1] == ["skill_7", 2, 0, false], "grade-specific FUNC_SKILL restores the M/G/P motion grade before binding") view.motion_bound.emit("wait") _ck(gates.calls.size() == 3 and gates.calls.back() == [77, false, true, false, true], "skill motion tail reopens the TCP state gate") nw._apply_anim(view, NetWorld.FUNC_EMOTION, false, NetWorld.WALKMODE_RUN, false, 305) _ck(view.emotion_calls.size() == 1 and view.emotion_calls[0] == [305, -1], "FUNC_EMOTION uses uArg as the one-shot motion id") _ck(gates.calls.size() == 4 and gates.calls.back() == [77, false, true, true, false], "active remote emotion disables later TCP processing") view.motion_bound.emit("wait") _ck(gates.calls.size() == 5 and gates.calls.back() == [77, false, true, false, true], "emotion motion tail reopens TCP processing") # GC_SYNC_POSITION enters TEMP_Push: a large correction triggers the # DAMAGE_FLYING -> STAND_UP chain, while the presentation offset starts at # the old position and eases back to the authoritative target. view.set_meta("vid", 77) view.set_meta("sync_push_serial", 0) gates.entities[77] = {"vid": 77, "pos": Vector3(3.0, 0.0, 0.0), "pos_cm": Vector3(300.0, 0.0, 0.0), "sync_push_serial": 1, "sync_push_from_cm": Vector3.ZERO, "sync_push_large": true, "sync_push_skill_active": false} nw._on_moved(77) _ck(view.sync_push_calls == 1, "large sync correction starts TEMP_Push reaction") _ck(gates.knockdown_calls.size() == 1 and gates.knockdown_calls[0] == [77, true], "large sync correction closes the knockdown network-state gate") var corrected: Vector3 = nw._sync_pushed_position(77, gates.entities[77], 0.0, MapCoord.to_world(Vector3(3.0, 0.0, 0.0))) _ck(is_equal_approx(corrected.x, 0.0), "TEMP_Push starts from the pre-correction presentation position") view.motion_bound.emit("wait") _ck(gates.knockdown_calls.size() == 2 and gates.knockdown_calls.back() == [77, false], "sync push motion tail releases the knockdown gate") # A physical obstacle aborts TEMP_Push at the first blocked sample and must # leave the authoritative target in charge without starting a second motion. var obstacle := CollisionWorld.new() get_root().add_child(obstacle) nw.world = obstacle var blocked_entity: Dictionary = gates.entities[77] blocked_entity["sync_push_serial"] = 2 gates.entities[77] = blocked_entity nw._on_moved(77) _ck(obstacle.samples > 1, "TEMP_Push probes the correction path in segments") _ck(not nw._sync_push.has(77) and view.sync_push_calls == 1, "blocked TEMP_Push resets blending without replaying knockdown") # A spawn can already carry an action state. The reference binds that motion # during actor creation; it must not wait for a later func edge. var nw2: Node = NetWorld.new() get_root().add_child(nw2) var mount := Node3D.new() get_root().add_child(mount) var spawn_client := GateClient.new() get_root().add_child(spawn_client) nw2.client = spawn_client nw2.parent = mount nw2.set_model_factory(func(_d: Dictionary) -> Node3D: return ActionView.new()) var initial := {"vid": 88, "name": "initial", "race": 101, "ch_type": 2, "func": NetWorld.FUNC_COMBO, "func_arg": 15, "moving": false, "walk_mode": NetWorld.WALKMODE_RUN, "attack_speed": 125, "hp": 100, "max_hp": 100, "dead": false, "pos": Vector3.ZERO, "parts": [0, 0, 0, 0], "affect_flags": 0} spawn_client.entities[88] = initial nw2._on_spawn(initial) var initial_view := nw2.node_for(88) as ActionView _ck(initial_view != null and initial_view.attack_calls.size() == 1 and initial_view.attack_calls[0] == [3, 15, 1.25], "spawned FUNC_COMBO binds immediately after actor fields are applied") nw.free() nw2.free() obstacle.free() mount.free() spawn_client.free() gates.free() view.free() await process_frame