Files
agame/tests/test_net_replication.gd
T
2026-09-23 16:27:02 +08:00

149 lines
5.5 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@tool
extends McpTestSuite
## 回归测试:Player 的同步配置与动作库必须真的被解析。
##
## 两处典型的「静默失效」:
## 1. player_sync.tres 键名写错 / replication_mode 落到 NEVER
## → 远端玩家僵在原地,而且不打任何错误。
## 2. 动作库是从 .glb 拷进来的,track 路径带一层 character-male-e/ 前缀。
## 一旦模型换名、层级变动、蒙皮骨骼改名,AnimationPlayer 会「有片段但什么都播不出来」,
## 同样不报错。
const CONFIG_PATH := "res://character/player/player_sync.tres"
const PLAYER_SCENE_PATH := "res://character/player/player.tscn"
const ANIM_PLAYER_PATH := "Body/character-male-e2/AnimationPlayer"
## player.gd 的 ANIM_CLIPS 写死了这几个名字,少一个对应状态就会静默不动。
const REQUIRED_CLIPS: Array[String] = ["idle", "walk", "sprint", "jump", "fall"]
## 必须被复制到远端的字段。远端物理与输入都关着,全靠这几个值回放。
const REQUIRED_SYNCED: Array[String] = [
".:position",
"Body:rotation",
".:health",
".:anim_state",
".:anim_speed",
]
func suite_name() -> String:
return "net"
func test_config_has_required_synced_fields() -> void:
var config := _load_config()
assert_true(config != null, "%s 未能加载为 SceneReplicationConfig" % CONFIG_PATH)
if config == null:
return
var paths := config.get_properties()
for required in REQUIRED_SYNCED:
assert_true(
config.has_property(NodePath(required)),
"缺少同步字段 %s,实际为 %s" % [required, paths]
)
func test_config_replication_mode_is_always() -> void:
var config := _load_config()
assert_true(config != null, "%s 未能加载" % CONFIG_PATH)
if config == null:
return
var paths := config.get_properties()
assert_gt(paths.size(), 0, "同步字段为空,远端玩家不会移动")
for path in paths:
assert_eq(
config.property_get_replication_mode(path),
SceneReplicationConfig.REPLICATION_MODE_ALWAYS,
"%s 的 replication_mode 不是 ALWAYS(改为 NEVER 会静默断掉同步)" % path
)
func test_player_scene_wires_synchronizer() -> void:
var player := _instantiate_player()
if player == null:
return
var sync := player.get_node_or_null("Sync") as MultiplayerSynchronizer
assert_true(sync != null, "Player 下缺少 Sync 节点")
if sync == null:
return
assert_true(sync.replication_config != null, "Sync 没有绑定 SceneReplicationConfig")
assert_eq(sync.root_path, ^"..", "Sync.root_path 应为 .. (相对 Player)")
if sync.replication_config != null:
assert_eq(
sync.replication_config.resource_path,
CONFIG_PATH,
"Sync 绑定的是别的同步配置:%s" % sync.replication_config.resource_path
)
func test_required_animation_clips_exist() -> void:
var anim_player := _find_animation_player()
if anim_player == null:
return
for clip in REQUIRED_CLIPS:
assert_true(
anim_player.has_animation(clip),
"动作库里缺少片段 %s(player.gd 的 ANIM_CLIPS 会指向不存在的片段)" % clip
)
## 片段是从 glb 里拷出来的,track 路径带层级前缀。换模型/改层级后整组 track 会一起失效,
## 而 AnimationPlayer 不报任何错 —— 现象是「人站着不动」。用测试钉住路径真的能解析。
func test_animation_track_paths_resolve() -> void:
var anim_player := _find_animation_player()
if anim_player == null:
return
var root := anim_player.get_node_or_null(anim_player.root_node)
assert_true(root != null, "AnimationPlayer.root_node 解析不到:%s" % anim_player.root_node)
if root == null:
return
# 逐帧验证 5 个要用到的片段,任何一个 track 断了都会让该动作失效
for clip in REQUIRED_CLIPS:
var anim := anim_player.get_animation(clip)
if anim == null:
continue
assert_gt(anim.get_track_count(), 0, "%s 片段没有任何 track" % clip)
for i in anim.get_track_count():
var track_path := anim.track_get_path(i)
# 不能直接 get_node(track_path):路径含 ":骨骼名" 属性部分,get_node 只认节点段
var owner_node := root.get_node_or_null(NodePath(track_path.get_concatenated_names()))
assert_true(
owner_node != null,
"%s 的 track %s 在 %s 下解析不到(动画会静默无效)" % [clip, track_path, anim_player.root_node]
)
if owner_node == null:
continue
# 属性段是骨骼名,模型换皮时要确保骨骼名没变
var bone := str(track_path.get_concatenated_subnames())
if owner_node is Skeleton3D and not bone.is_empty():
assert_true(
(owner_node as Skeleton3D).find_bone(bone) != -1,
"%s 引用了骨骼 %s,但骨架里没有这根骨头(模型换过了?)" % [clip, bone]
)
## 直接用 load() 拿到的是编辑器进程里的内存副本 —— 外部刚改过磁盘时它可能还是旧的,
## 测试会因此假绿或假红。这里强制按磁盘重读,断言的是文件本身。
func _load_config() -> SceneReplicationConfig:
return ResourceLoader.load(CONFIG_PATH, "", ResourceLoader.CACHE_MODE_REPLACE) as SceneReplicationConfig
func _find_animation_player() -> AnimationPlayer:
var player := _instantiate_player()
if player == null:
return null
var anim_player := player.get_node_or_null(ANIM_PLAYER_PATH) as AnimationPlayer
assert_true(anim_player != null, "找不到 %s" % ANIM_PLAYER_PATH)
return anim_player
func _instantiate_player() -> Node:
var packed := load(PLAYER_SCENE_PATH) as PackedScene
assert_true(packed != null, "%s 未能加载" % PLAYER_SCENE_PATH)
if packed == null:
return null
var player := packed.instantiate()
track(player)
return player