Metin2 game client (P0–P11) + mobile asset pipeline

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
This commit is contained in:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+92
View File
@@ -0,0 +1,92 @@
# gamescene_test —— game_scene.gd 装配的 headless 自检(无服务器 / 无地图)。
# godot --headless --path project --script gamescene_test.gd
# 假 M2Client 驱动:断言场景装出来、子系统接线、实体镜像、主角定位。
extends SceneTree
const GameScene = preload("res://game_scene.gd")
class FakeClient extends Node:
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 points_changed(points: Dictionary)
signal target_info(vid: int, hp_percent: int)
signal chat(type: int, vid: int, text: String)
signal damage(vid: int, amount: int, flag: int)
var ents := {}
var main := 0
func is_in_game() -> bool: return true
func get_main_vid() -> int: return main
func get_entity(vid: int) -> Dictionary: return ents.get(vid, {})
func move(_f, _a, _r, _x, _y) -> bool: return true
func attack(_m, _v) -> bool: return true
func set_target(_v) -> bool: return true
func spawn(vid, nm, pos, is_main := false):
ents[vid] = {"vid": vid, "name": nm, "pos": pos, "is_main": is_main,
"func": 0, "moving": false, "angle_deg": 0.0, "hp": 100, "max_hp": 100, "dead": false}
if is_main:
main = vid
entity_main_set.emit(vid)
entity_spawned.emit(ents[vid])
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
func _init() -> void:
await _run()
if _fail == 0:
print("PASS: gamescene_test (assembly + wiring + entity mirror + spawn locate)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
var fc := FakeClient.new()
get_root().add_child(fc)
# 主角先于场景就位(模拟重连 / 已在局内)
fc.spawn(1000, "Me", Vector3(20, 0, -30), true)
var gs: Node3D = GameScene.new()
get_root().add_child(gs)
await gs.setup(fc, "", "no_such_map") # setup 现在是协程(分帧避免阻塞漏 PONG)
await process_frame
# 1) 子系统都装出来了
_ck(gs.player != null, "player built")
_ck(gs.cam != null and gs.cam.current, "camera current")
_ck(gs.pc != null, "player_controller built")
_ck(gs.net_world != null, "net_world built")
_ck(gs.net_play != null, "net_play built")
_ck(gs.hud != null, "hud built")
_ck(gs.atlas_ui != null, "atlas_ui built")
_ck(gs.get_node_or_null("Entities") != null, "entity mount present")
# 2) 主角定位到出生点(load_map 失败 -> sample_height 返回 0 -> y=0
_ck(gs.player.position.distance_to(Vector3(20, 0, 30)) < 0.01, # MapCoord.to_world 翻 Z
"local player placed at spawn (world 帧), got %s" % gs.player.position)
# 3) net_world 不给主角画节点(本地 player 代表)
await process_frame
_ck(gs.net_world.node_for(1000) == null, "no net_world node for local vid")
# 4) 别的实体会被镜像成挂载节点
fc.spawn(2000, "Wolf", Vector3(25, 0, -30))
await process_frame
var mob: Node3D = gs.net_world.node_for(2000)
_ck(mob != null and mob.get_parent() == gs.get_node("Entities"), "remote entity mirrored under mount")
# 5) HUD 活着,points 驱动不报错
fc.points_changed.emit({"hp": 60, "max_hp": 100, "sp": 20, "max_sp": 50,
"exp": 10, "next_exp": 100, "level": 3})
await process_frame
_ck(true, "points_changed handled without error")
gs.queue_free()