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:
@@ -0,0 +1,233 @@
|
||||
# UiBuild (P1) —— uiscript 嵌套 Dictionary → Godot Control 树。
|
||||
#
|
||||
# var spec := UiScript.new().parse_file(path)
|
||||
# var r := UiBuild.build(spec, assets_root)
|
||||
# add_child(r.root)
|
||||
# r.nodes["accept"].pressed.connect(...) # 按 uiscript 里的 "name" 取节点
|
||||
#
|
||||
# 覆盖 uiscript 最常用的 ~18 种 type;其它降级为 Control / Button / TextureRect。
|
||||
# board/thinboard 的九宫格用 ui_kit(真实 ymir 贴图,缺失则 StyleBoxFlat 兜底)。
|
||||
extends RefCounted
|
||||
|
||||
const UiKit = preload("res://ui_kit.gd")
|
||||
const UiAssets = preload("res://ui/ui_assets.gd")
|
||||
|
||||
# spec: 顶层 window dict。返回 { root: Control, nodes: {name: Control} }
|
||||
static func build(spec: Dictionary, assets_root: String) -> Dictionary:
|
||||
var ctx := {"assets": assets_root, "nodes": {}}
|
||||
var root := _make(spec, ctx, Vector2i.ZERO)
|
||||
if root == null:
|
||||
root = Control.new()
|
||||
return {"root": root, "nodes": ctx["nodes"]}
|
||||
|
||||
# --- recursion ---------------------------------------------------------
|
||||
|
||||
static func _i(spec: Dictionary, key: String, def := 0) -> int:
|
||||
var v = spec.get(key, def)
|
||||
return int(v) if (v is int or v is float or v is bool or v is String) else def
|
||||
|
||||
static func _str(v) -> String:
|
||||
if v == null:
|
||||
return ""
|
||||
return v if v is String else str(v)
|
||||
|
||||
static func _make(spec: Dictionary, ctx: Dictionary, parent_size: Vector2i) -> Control:
|
||||
var type := _str(spec.get("type", "window"))
|
||||
var w := _i(spec, "width", 0)
|
||||
var h := _i(spec, "height", 0)
|
||||
var node := _make_by_type(type, spec, ctx, Vector2i(w, h))
|
||||
if node == null:
|
||||
node = Control.new()
|
||||
|
||||
node.name = _safe_name(_str(spec.get("name", type)))
|
||||
if w > 0:
|
||||
node.custom_minimum_size = Vector2(w, h)
|
||||
node.size = Vector2(w, h)
|
||||
|
||||
# 位置:x/y + horizontal_align / vertical_align(相对父)
|
||||
var x := _i(spec, "x", 0)
|
||||
var y := _i(spec, "y", 0)
|
||||
match _str(spec.get("horizontal_align", "left")):
|
||||
"center": x = (parent_size.x - w) / 2 + x
|
||||
"right": x = parent_size.x - w - x
|
||||
match _str(spec.get("vertical_align", "top")):
|
||||
"center": y = (parent_size.y - h) / 2 + y
|
||||
"bottom": y = parent_size.y - h - y
|
||||
node.position = Vector2(x, y)
|
||||
|
||||
var nm := _str(spec.get("name", ""))
|
||||
if nm != "":
|
||||
ctx["nodes"][nm] = node
|
||||
|
||||
var self_size := Vector2i(w, h) if w > 0 else parent_size
|
||||
for child in spec.get("children", []):
|
||||
if child is Dictionary:
|
||||
var c := _make(child, ctx, self_size)
|
||||
if c:
|
||||
node.add_child(c)
|
||||
return node
|
||||
|
||||
static func _make_by_type(type: String, spec: Dictionary, ctx: Dictionary, sz: Vector2i) -> Control:
|
||||
var assets: String = ctx["assets"]
|
||||
match type:
|
||||
"window", "box":
|
||||
var c := Control.new()
|
||||
c.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
return c
|
||||
"board", "thinboard", "board_with_titlebar":
|
||||
var p := _board(assets, "thinboard" if type == "thinboard" else "board", sz)
|
||||
if type == "board_with_titlebar" and spec.has("title"):
|
||||
var tl := Label.new()
|
||||
tl.name = "TitleText"
|
||||
tl.text = _str(spec.get("title", ""))
|
||||
tl.position = Vector2(12, 6)
|
||||
p.add_child(tl)
|
||||
p.set_meta("titlebar_h", 24)
|
||||
return p
|
||||
"titlebar":
|
||||
var t := Panel.new()
|
||||
t.set_meta("is_titlebar", true)
|
||||
return t
|
||||
"text":
|
||||
var lbl := Label.new()
|
||||
lbl.text = _str(spec.get("text", ""))
|
||||
if spec.has("text_color"):
|
||||
lbl.add_theme_color_override("font_color", _argb(int(spec["text_color"])))
|
||||
match _str(spec.get("text_horizontal_align", "left")):
|
||||
"center": lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
"right": lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
return lbl
|
||||
"button", "toggle_button":
|
||||
var b := Button.new()
|
||||
b.text = _str(spec.get("text", ""))
|
||||
b.toggle_mode = (type == "toggle_button")
|
||||
var nrm := UiAssets.load_tex(assets, _str(spec.get("default_image", "")))
|
||||
var ovr := UiAssets.load_tex(assets, _str(spec.get("over_image", "")))
|
||||
var dwn := UiAssets.load_tex(assets, _str(spec.get("down_image", "")))
|
||||
if nrm:
|
||||
b.add_theme_stylebox_override("normal", _sb_tex(nrm))
|
||||
if ovr: b.add_theme_stylebox_override("hover", _sb_tex(ovr))
|
||||
if dwn: b.add_theme_stylebox_override("pressed", _sb_tex(dwn))
|
||||
return b
|
||||
"radio_button":
|
||||
var cb := CheckBox.new()
|
||||
cb.text = _str(spec.get("text", ""))
|
||||
return cb
|
||||
"image", "expanded_image", "ani_image", "mark":
|
||||
var ir := TextureRect.new()
|
||||
ir.texture = UiAssets.load_tex(assets, _str(spec.get("image", "")))
|
||||
ir.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
ir.stretch_mode = TextureRect.STRETCH_SCALE if type == "expanded_image" \
|
||||
else TextureRect.STRETCH_KEEP
|
||||
ir.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
return ir
|
||||
"editline":
|
||||
var e := LineEdit.new()
|
||||
e.text = _str(spec.get("text", ""))
|
||||
e.secret = bool(spec.get("secret", false))
|
||||
if spec.has("input_limit"):
|
||||
e.max_length = int(spec["input_limit"])
|
||||
return e
|
||||
"line":
|
||||
var ln := ColorRect.new()
|
||||
ln.color = Color(1, 1, 1, 0.15)
|
||||
if _i(spec, "height", 0) == 0:
|
||||
ln.custom_minimum_size = Vector2(_i(spec, "width", 100), 1)
|
||||
return ln
|
||||
"listbox", "candidate_list":
|
||||
return ItemList.new()
|
||||
"gauge", "bar", "horizontalbar":
|
||||
var pb := ProgressBar.new()
|
||||
pb.show_percentage = false
|
||||
pb.value = 100.0
|
||||
return pb
|
||||
"grid_table", "slotbar", "slot":
|
||||
# Metin2 物品格容器:有 start_index/x_count 或显式 "slot" 元组 -> 建子格;
|
||||
# 否则 grid_table 退化成普通 GridContainer。
|
||||
var cells := _slot_cells(spec)
|
||||
if cells.is_empty() and type == "grid_table":
|
||||
var g := GridContainer.new()
|
||||
g.columns = maxi(1, _i(spec, "x_count", _i(spec, "column", 1)))
|
||||
return g
|
||||
var s := Control.new()
|
||||
s.set_meta("is_slot_container", true)
|
||||
for cd in cells:
|
||||
var cell := Panel.new()
|
||||
cell.name = "slot_%d" % int(cd["index"])
|
||||
cell.position = Vector2(cd["x"], cd["y"])
|
||||
cell.custom_minimum_size = Vector2(cd["w"], cd["h"])
|
||||
cell.size = Vector2(cd["w"], cd["h"])
|
||||
cell.set_meta("slot_index", int(cd["index"]))
|
||||
cell.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
s.add_child(cell)
|
||||
return s
|
||||
"sliderbar":
|
||||
return HSlider.new()
|
||||
"scrollbar", "thin_scrollbar":
|
||||
return VScrollBar.new()
|
||||
_:
|
||||
return Control.new()
|
||||
|
||||
# --- helpers ---------------------------------------------------------
|
||||
|
||||
static func _board(assets: String, prefix: String, sz: Vector2i) -> Panel:
|
||||
var p := Panel.new()
|
||||
var np: NinePatchRect = null
|
||||
if assets != "":
|
||||
np = UiKit.board(assets, prefix, 32, 128)
|
||||
if np and np.texture:
|
||||
np.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
np.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
p.add_child(np)
|
||||
p.get_child(0).show_behind_parent = true
|
||||
else:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = Color(0.10, 0.11, 0.14, 0.92)
|
||||
sb.border_color = Color(0.35, 0.32, 0.26)
|
||||
sb.set_border_width_all(1)
|
||||
sb.set_corner_radius_all(3)
|
||||
p.add_theme_stylebox_override("panel", sb)
|
||||
return p
|
||||
|
||||
# slot 容器的子格:显式 "slot" 元组,或 start_index/x_count/y_count/x_step/y_step 网格。
|
||||
static func _slot_cells(spec: Dictionary) -> Array:
|
||||
var cells := []
|
||||
if spec.get("slot", null) is Array:
|
||||
for sd in spec["slot"]:
|
||||
if sd is Dictionary:
|
||||
cells.append({"index": _i(sd, "index"), "x": _i(sd, "x"), "y": _i(sd, "y"),
|
||||
"w": _i(sd, "width", 32), "h": _i(sd, "height", 32)})
|
||||
elif spec.has("x_count"):
|
||||
var si := _i(spec, "start_index", 0)
|
||||
var xc := maxi(1, _i(spec, "x_count", 1))
|
||||
var yc := maxi(1, _i(spec, "y_count", 1))
|
||||
var xs := _i(spec, "x_step", 32)
|
||||
var ys := _i(spec, "y_step", 32)
|
||||
for row in yc:
|
||||
for col in xc:
|
||||
cells.append({"index": si + row * xc + col, "x": col * xs, "y": row * ys,
|
||||
"w": 32, "h": 32})
|
||||
return cells
|
||||
|
||||
static func _sb_tex(t: Texture2D) -> StyleBoxTexture:
|
||||
var sb := StyleBoxTexture.new()
|
||||
sb.texture = t
|
||||
return sb
|
||||
|
||||
static func _argb(v: int) -> Color:
|
||||
# 0xAARRGGBB
|
||||
var a := (v >> 24) & 0xff
|
||||
var r := (v >> 16) & 0xff
|
||||
var g := (v >> 8) & 0xff
|
||||
var b := v & 0xff
|
||||
if a == 0:
|
||||
a = 255
|
||||
return Color8(r, g, b, a)
|
||||
|
||||
static func _safe_name(s: String) -> String:
|
||||
var out := s
|
||||
for bad: String in [".", ":", "@", "/", "%", " "]:
|
||||
out = out.replace(bad, "_")
|
||||
return out if out != "" else "node"
|
||||
Reference in New Issue
Block a user