Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
phases, EntityStore world model, ~all GC/CG headers. char create/delete,
private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
token), system-option + game-option + ESC system menu, private-shop 39-grid,
party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.
Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.
Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).
ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
117 lines
4.5 KiB
GDScript
117 lines
4.5 KiB
GDScript
# chat_test —— P3:聊天窗路由 + 前缀解析 + 气泡 headless 自检。
|
|
# godot --headless --path project --script chat_test.gd
|
|
extends SceneTree
|
|
|
|
const ChatUI = preload("res://ui/chat_ui.gd")
|
|
const NetWorld = preload("res://net_world.gd")
|
|
|
|
class FakeClient extends Node:
|
|
signal chat(type: int, vid: int, text: String)
|
|
# 注:GDScript 不允许 signal 和 func 同名;真 M2Client 是 GDExtension,两者可共存。
|
|
# 这里 whisper 用作方法(chat_ui 调 client.whisper(...));入站 whisper 直接调 chat._on_whisper。
|
|
signal item_picked_up(vnum: int, count: int, from: String)
|
|
signal fishing_event(subheader: int, info: int, dir: int)
|
|
# net_world 也连这些:
|
|
signal entity_spawned(entity: Dictionary)
|
|
signal entity_despawned(vid: int)
|
|
signal entity_moved(vid: int)
|
|
signal entity_main_set(vid: int)
|
|
signal entity_dead(vid: int)
|
|
signal vitals_changed(vid: int)
|
|
signal damage(vid: int, amount: int, flag: int)
|
|
var ents := {1000: {"vid": 1000, "name": "Me"}, 2000: {"vid": 2000, "name": "Bob"}}
|
|
var said := []
|
|
var whispered := []
|
|
func get_entity(vid: int) -> Dictionary: return ents.get(vid, {})
|
|
func get_entities() -> Array: return ents.values()
|
|
func say(t, s) -> bool: said.append([t, s]); return true
|
|
func whisper(to, s) -> bool: whispered.append([to, s]); return true
|
|
|
|
var _fail := 0
|
|
func _ck(c: bool, m: String) -> void:
|
|
if not c:
|
|
_fail += 1
|
|
printerr("FAIL: " + m)
|
|
|
|
func _init() -> void:
|
|
_run()
|
|
if _fail == 0:
|
|
print("PASS: chat_test (routing + prefixes + bubble)")
|
|
quit(0)
|
|
else:
|
|
printerr("%d check(s) failed" % _fail)
|
|
quit(1)
|
|
|
|
func _last(rt: RichTextLabel) -> String:
|
|
return rt.get_parsed_text().strip_edges().split("\n")[-1]
|
|
|
|
func _run() -> void:
|
|
var canvas := CanvasLayer.new()
|
|
get_root().add_child(canvas)
|
|
var fc := FakeClient.new()
|
|
get_root().add_child(fc)
|
|
var chat: Node = ChatUI.new()
|
|
get_root().add_child(chat)
|
|
chat.setup(fc, canvas)
|
|
|
|
_ck(chat._log.size() == 4, "4 tabs built")
|
|
var tAll: RichTextLabel = chat._log[0]
|
|
var tWhisper: RichTextLabel = chat._log[1]
|
|
var tSys: RichTextLabel = chat._log[2]
|
|
var tBattle: RichTextLabel = chat._log[3]
|
|
|
|
# 普通聊天 -> 全部标签,带名字
|
|
fc.chat.emit(0, 2000, "hi all")
|
|
_ck(_last(tAll).contains("Bob: hi all"), "talking -> All tab with name")
|
|
_ck(chat._buffers[2].size() == 0, "talking not in System tab")
|
|
|
|
# info -> 系统 + 全部
|
|
fc.chat.emit(1, 0, "you leveled up")
|
|
_ck(_last(tSys).contains("you leveled up"), "info -> System tab")
|
|
_ck(_last(tAll).contains("you leveled up"), "info -> All tab too")
|
|
|
|
# whisper -> 私聊 + 全部(入站直接调 handler,见 FakeClient 注释)
|
|
chat._on_whisper(0, "Alice", "secret")
|
|
_ck(_last(tWhisper).contains("Alice: secret"), "whisper -> Whisper tab")
|
|
_ck(_last(tAll).contains("Alice: secret"), "whisper -> All tab")
|
|
chat._on_whisper(1, "", "server notice")
|
|
_ck(_last(tWhisper).contains("[系统]: server notice"), "system whisper -> [系统]")
|
|
|
|
# 拾取 -> 系统 + 战斗
|
|
fc.item_picked_up.emit(27, 3, "")
|
|
_ck(_last(tSys).contains("拾取 #27 x3"), "pickup -> System")
|
|
_ck(_last(tBattle).contains("拾取 #27 x3"), "pickup -> Battle")
|
|
# 钓鱼服务端事件:成功/鱼获进入系统和战斗日志。
|
|
fc.fishing_event.emit(3, 1000, 0)
|
|
_ck(_last(tSys).contains("鱼钩命中"), "fishing success -> System")
|
|
fc.fishing_event.emit(5, 30001, 0)
|
|
_ck(_last(tBattle).contains("捕获物品 #30001"), "fishing item -> Battle")
|
|
|
|
# --- 前缀解析 ---
|
|
chat._on_submit("hello world")
|
|
_ck(fc.said.size() == 1 and fc.said[0] == [0, "hello world"], "plain -> say(TALKING)")
|
|
chat._on_submit("/g guild msg")
|
|
_ck(fc.said.size() == 2 and fc.said[1] == [4, "guild msg"], "/g -> say(GUILD)")
|
|
chat._on_submit("/s shout!")
|
|
_ck(fc.said[2] == [6, "shout!"], "/s -> say(SHOUT)")
|
|
chat._on_submit("/w Bob psst hey")
|
|
_ck(fc.whispered.size() == 1 and fc.whispered[0] == ["Bob", "psst hey"], "/w Bob ... -> whisper('Bob','psst hey')")
|
|
_ck(_last(tWhisper).contains("→ Bob: psst hey"), "/w -> local echo in Whisper tab")
|
|
|
|
# --- 气泡 ---
|
|
var nw: Node = NetWorld.new()
|
|
get_root().add_child(nw)
|
|
var mount := Node3D.new()
|
|
get_root().add_child(mount)
|
|
nw.setup(fc, mount)
|
|
fc.entity_spawned.emit({"vid": 2000, "name": "Bob", "pos": Vector3.ZERO, "func": 0, "angle_deg": 0.0})
|
|
await process_frame
|
|
var got_main := [""]
|
|
nw.main_bubble.connect(func(t): got_main[0] = t)
|
|
nw._bubble(2000, "hi there")
|
|
var bob: Node3D = nw.node_for(2000)
|
|
_ck(bob != null and bob.get_node_or_null("bubble") != null, "entity 2000 got a bubble node")
|
|
nw.set_local_vid(1000)
|
|
nw._bubble(1000, "my line")
|
|
_ck(got_main[0] == "my line", "local vid bubble -> main_bubble signal")
|