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
+79
View File
@@ -0,0 +1,79 @@
# skill_fx_test —— 技能特效表(skill id / 内建 id → .mseheadless 自检。
# godot --headless --path project --script skill_fx_test.gd
extends SceneTree
const SkillFx = preload("res://fx/skill_fx.gd")
const SkillTable = preload("res://ui/skill_table.gd")
# 假 EffectRegistry:记录 resolve / spawn,指定哪些名字「存在」
class FakeFxr extends RefCounted:
var exists := {} # name -> true
var spawned := []
func resolve(name: String) -> String:
return "res://fake/%s.mse" % name if exists.get(name, false) else ""
func spawn(name: String, _parent, _one := true):
spawned.append(name)
return null
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: skill_fx_test")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
# 用真 skilldesc.txt(有就用,没有用桩表)
var st: RefCounted = SkillTable.new()
var assets := AssetRoot.path()
var have_real := false
for lang in ["en", "common"]:
if st.load_file(assets.path_join("locale/locale/%s/skilldesc.txt" % lang)):
have_real = true
break
var fxr := FakeFxr.new()
var host := Node3D.new()
get_root().add_child(host)
var sfx: RefCounted = SkillFx.new()
sfx.setup(fxr, st)
if have_real:
# skill 1 = Three-Way Cut, motion_name samyeon
# 后缀 = clamp(2 + master, 2, 4)m0->_2, m1->_3, m2/m3->_4
_ck(sfx.skill_effect_name(1, 0) == "samyeon_2", "skill 1 m0 -> samyeon_2")
_ck(sfx.skill_effect_name(1, 1) == "samyeon_3", "skill 1 m1(M) -> samyeon_3")
_ck(sfx.skill_effect_name(1, 2) == "samyeon_4", "skill 1 m2(G) -> samyeon_4")
_ck(sfx.skill_effect_name(1, 9) == "samyeon_4", "master 夹到 4")
# 主特效存在 + 一个子特效存在 -> 播 2 个
fxr.exists = {"samyeon_4": true, "samyeon_blow_4": true, "samyeon_hand_4": false}
var n: int = sfx.spawn_skill(1, 3, host)
_ck(n == 2 and fxr.spawned.has("samyeon_4") and fxr.spawned.has("samyeon_blow_4"),
"spawn_skill 播主 + 存在的子特效 (%d: %s)" % [n, fxr.spawned])
# motion 查不到 -> 空
_ck(sfx.skill_effect_name(99999, 0) == "", "未知 skill -> 空名")
else:
print(" (skip real skilldesc checks: 无资产)")
# 桩:手塞一条
st._by_id[7] = {"id": 7, "motion": "testmo", "job": "WARRIOR", "name": "T"}
_ck(sfx.skill_effect_name(7, 1) == "testmo_2", "桩 skill -> testmo_2")
# 内建特效:SE_AUTO_HPUP(19) -> drugup_red
fxr.spawned.clear()
fxr.exists = {"drugup_red": true, "drugup_blue": true}
_ck(sfx.spawn_special(19, host) and fxr.spawned == ["drugup_red"],
"special 19 (SE_AUTO_HPUP) -> drugup_red")
_ck(sfx.spawn_special(20, host) and fxr.spawned[-1] == "drugup_blue",
"special 20 (SE_AUTO_SPUP) -> drugup_blue")
# 没映射的 id / 文件不存在 -> false
_ck(sfx.spawn_special(255, host) == false, "未映射 special id -> false")
fxr.exists = {}
_ck(sfx.spawn_special(19, host) == false, "文件不存在 -> false")