class_name PlayableProbe extends Node ## 只读网络/场景观察器。所有服务端证据必须从 M2Client 信号或状态快照进入, ## 本节点不调用移动、攻击、拾取、施法或其它会改变服务器状态的方法。 signal observed(kind: String, payload: Dictionary) var client: Node var flow: Node var connection_epoch := 0 var _connections: Array[Dictionary] = [] func setup(app_flow: Node, m2client: Node) -> void: flow = app_flow client = m2client if client == null: return _bind("entered_game", Callable(self, "_on_entered_game")) _bind("disconnected", Callable(self, "_on_disconnected")) _bind("char_list", Callable(self, "_on_char_list")) _bind("entity_spawned", Callable(self, "_on_entity_spawned")) _bind("entity_despawned", Callable(self, "_on_entity_despawned")) _bind("entity_moved", Callable(self, "_on_entity_moved")) _bind("entity_dead", Callable(self, "_on_entity_dead")) _bind("damage", Callable(self, "_on_damage")) _bind("vitals_changed", Callable(self, "_on_vitals")) _bind("ground_item_added", Callable(self, "_on_ground_item_added")) _bind("ground_item_removed", Callable(self, "_on_ground_item_removed")) _bind("item_picked_up", Callable(self, "_on_item_picked_up")) _bind("inventory_changed", Callable(self, "_on_inventory_changed")) _bind("effect_cue", Callable(self, "_on_effect_cue")) _bind("fly_cue", Callable(self, "_on_fly_cue")) _bind("fly_targeting", Callable(self, "_on_fly_targeting")) func snapshot() -> Dictionary: var in_game: bool = client != null and client.has_method("is_in_game") and client.is_in_game() var main_vid := int(client.get_main_vid()) if client != null and client.has_method("get_main_vid") else 0 var entity_count: int = client.get_entities().size() if client != null and client.has_method("get_entities") else 0 var ground_count: int = client.get_ground_items().size() if client != null and client.has_method("get_ground_items") else 0 var state := int(flow.state()) if flow != null and flow.has_method("state") else -1 return {"in_game": in_game, "main_vid": main_vid, "entity_count": entity_count, "ground_item_count": ground_count, "app_state": state, "connection_epoch": connection_epoch} func disconnect_all() -> void: for entry in _connections: var signal_name: String = entry.signal var callback: Callable = entry.callback if client != null and client.has_signal(signal_name) and client.is_connected(signal_name, callback): client.disconnect(signal_name, callback) _connections.clear() func _exit_tree() -> void: disconnect_all() func _bind(signal_name: String, callback: Callable) -> void: if client.has_signal(signal_name) and not client.is_connected(signal_name, callback): client.connect(signal_name, callback) _connections.append({"signal": signal_name, "callback": callback}) func _emit(kind: String, payload := {}) -> void: var data: Dictionary = payload.duplicate(true) data["connection_epoch"] = connection_epoch data["received_us"] = Time.get_ticks_usec() observed.emit(kind, data) func _on_entered_game() -> void: connection_epoch += 1 _emit("entered_game") func _on_disconnected(reason: String) -> void: _emit("disconnected", {"reason": reason}) func _on_char_list(chars: Array) -> void: _emit("char_list", {"count": chars.size()}) func _on_entity_spawned(entity: Dictionary) -> void: _emit("entity_spawned", {"vid": int(entity.get("vid", 0)), "race": int(entity.get("race", 0)), "kind": int(entity.get("kind", -1))}) func _on_entity_despawned(vid: int) -> void: _emit("entity_despawned", {"vid": vid}) func _on_entity_moved(vid: int) -> void: _emit("entity_moved", {"vid": vid}) func _on_entity_dead(vid: int) -> void: _emit("entity_dead", {"vid": vid}) func _on_damage(vid: int, amount: int, flag: int) -> void: _emit("damage", {"vid": vid, "amount": amount, "flag": flag}) func _on_vitals(vid: int) -> void: _emit("vitals_changed", {"vid": vid}) func _on_ground_item_added(item: Dictionary) -> void: _emit("ground_item_added", {"vid": int(item.get("vid", 0)), "vnum": int(item.get("vnum", 0)), "owner": String(item.get("owner", ""))}) func _on_ground_item_removed(vid: int) -> void: _emit("ground_item_removed", {"vid": vid}) func _on_item_picked_up(vnum: int, count: int, source: String) -> void: _emit("item_picked_up", {"vnum": vnum, "count": count, "source": source}) func _on_inventory_changed(window: int, cell: int) -> void: _emit("inventory_changed", {"window": window, "cell": cell}) func _on_effect_cue(vid: int, name: String, special: int) -> void: _emit("effect_cue", {"vid": vid, "name": name, "special": special}) func _on_fly_cue(kind: int, start_vid: int, end_vid: int) -> void: _emit("fly_cue", {"type": kind, "start_vid": start_vid, "end_vid": end_vid}) func _on_fly_targeting(shooter_vid: int, target_vid: int, target_cm: Vector2, append: bool) -> void: _emit("fly_targeting", {"shooter_vid": shooter_vid, "target_vid": target_vid, "target_cm": [target_cm.x, target_cm.y], "append": append})