Files
mtgodot-poc/project/ui/ui_assets.gd
T
shenleiandClaude Opus 5.5 1fe8a4b643 2V2-e.2: Godot consumer of the 3D draw commands, offline screenshot
python_3d_surface.gd turns RenderGame's recorded draws into meshes under a
camera read back from SetPerspective; textures come from the 40250 packs.
GetFarClip and the CMapManager ctor ported verbatim. Until the environment
is ported, lit draws with no light/ambient are shown unshaded (stand-in).
script/python_game_render_test.sh runs system.py login -> select -> game
against port_fake_login_server (or --live via MT_LIVE_*) and screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:35:36 +09:00

176 lines
6.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UiAssets (P1) —— 解析 uiscript 里的图片路径(`.sub` / `.tga` / `.png` / `.jpg`)。
#
# 路径形如 `d:/ymir work/ui/public/middle_button_01.sub`。解析:
# 1) 去掉盘符前缀
# 2) 依次试: <assets>/<rel> · <assets>/<pack>/<rel>(散包目录)
# 3) `.sub` = 子图描述(title/image/left/top/right/bottom)→ 载入其 image + 裁剪成 AtlasTexture
# `.dds` 走 Metin2World.load_dds(C++ dxt 解码)运行时解成 Image。
extends RefCounted
static var _cache := {}
static func clear_cache() -> void:
_cache.clear()
# DDS 运行时解码:走 Metin2World.load_dds(C++ 里的 dxt.cpp,DXT1/3/5 + BGRA8)。
# Godot 无原生运行时 DDS 解码,UI 里的 select.dds / 职业名图都得走这条路。
static func load_dds_image(path: String) -> Image:
if path == "" or not FileAccess.file_exists(path):
return null
if not ClassDB.class_exists("Metin2World"):
return null
# Metin2World is a Node, not RefCounted; a static reference never frees it.
# The decoder returns an independently owned Image, so release the helper
# after each decode (textures themselves remain cached by load_tex).
var helper: Object = ClassDB.instantiate("Metin2World")
var img = helper.call("load_dds", path)
helper.free()
return img if img is Image else null
static func load_tex(assets_root: String, vpath: String) -> Texture2D:
if vpath == "" or assets_root == "":
return null
var key := assets_root + "|" + vpath
if _cache.has(key):
return _cache[key]
var tex: Texture2D = _load_uncached(assets_root, vpath)
_cache[key] = tex
return tex
# 只从 40250 Client/pack(Metin2Pack)取,不看 assets/ 散文件:移植的客户端从包里读模型,
# 贴图也必须是同一份(assets/ 里同名的 fork 贴图 UV 布局可能不同)。
static func load_pack_tex(vpath: String) -> Texture2D:
if vpath == "":
return null
var key := "pack|" + vpath
if _cache.has(key):
return _cache[key]
# 40250 的包名带盘符(d:/ymir work/...),原样交给 CEterPackManager。
var tex: Texture2D = _load_pack_image(vpath.replace("\\", "/"))
_cache[key] = tex
return tex
static func _strip_drive(p: String) -> String:
var s := p.replace("\\", "/")
if s.length() >= 2 and s[1] == ":":
s = s.substr(2)
return s.lstrip("/")
static func _resolve(assets_root: String, rel: String) -> String:
var direct := assets_root.path_join(rel)
if FileAccess.file_exists(direct):
return direct
# 散包:<assets>/<sub>/<rel>
var da := DirAccess.open(assets_root)
if da:
for sub in da.get_directories():
var cand := assets_root.path_join(sub).path_join(rel)
if FileAccess.file_exists(cand):
return cand
# 大小写不敏感兜底:<assets>/**/ymir work/ui/... —— 只按 basename 找
return ""
static func _load_uncached(assets_root: String, vpath: String) -> Texture2D:
var rel := _strip_drive(vpath)
var real := _resolve(assets_root, rel)
if real == "":
# 试把 .sub 换成 .tga / .png
for ext: String in [".tga", ".png", ".jpg"]:
real = _resolve(assets_root, rel.get_basename() + ext)
if real != "":
break
if real == "":
return _load_pack_image(rel)
if real.get_extension().to_lower() == "sub":
return _load_sub(real)
return _load_image_file(real)
static func _load_pack_image(rel: String) -> Texture2D:
if not ClassDB.class_exists("Metin2Pack") or not Metin2Pack.is_ready():
return null
if not Metin2Pack.exists(rel):
return null
var ext := rel.get_extension().to_lower()
var image: Image
if ext == "dds":
if not ClassDB.class_exists("Metin2World"):
return null
var helper: Object = ClassDB.instantiate("Metin2World")
image = helper.call("load_dds", "pack://" + rel)
helper.free()
else:
var bytes: PackedByteArray = Metin2Pack.get_bytes(rel)
image = Image.new()
var err := ERR_FILE_UNRECOGNIZED
match ext:
"tga": err = image.load_tga_from_buffer(bytes)
"png": err = image.load_png_from_buffer(bytes)
"jpg", "jpeg": err = image.load_jpg_from_buffer(bytes)
"bmp": err = image.load_bmp_from_buffer(bytes)
if err != OK:
return null
return ImageTexture.create_from_image(image) if image != null and not image.is_empty() else null
static func _load_image_file(path: String) -> Texture2D:
# .sub 文件可能引用了未随当前资源包发布的共享贴图(例如 Public.tga)。
# 先做存在性检查,避免 headless/UI fallback 因缺失可选贴图刷错误日志。
if not FileAccess.file_exists(path):
return null
var ext := path.get_extension().to_lower()
if ext == "dds":
var di := load_dds_image(path)
return ImageTexture.create_from_image(di) if di != null else null
var img := Image.new()
if img.load(path) != OK:
return null
return ImageTexture.create_from_image(img)
static func _load_sub(path: String) -> Texture2D:
var txt := FileAccess.get_file_as_string(path)
var image_name := ""
var l := 0
var t := 0
var r := -1
var b := -1
for line in txt.split("\n"):
var parts := line.strip_edges().split(" ", false)
if parts.size() < 2:
continue
match parts[0]:
"image": image_name = parts[1].strip_edges().trim_prefix('"').trim_suffix('"')
"left": l = int(parts[1])
"top": t = int(parts[1])
"right": r = int(parts[1])
"bottom": b = int(parts[1])
if image_name == "":
return null
# Skill .sub files live below ui/skill/<job>, while their shared DDS
# lives in ui/. Search the containing directory and its parents.
var img_path := ""
var search_dir := path.get_base_dir()
for _i in 6:
for candidate in [image_name, image_name.get_basename() + ".tga",
image_name.get_basename() + ".png", image_name.get_basename() + ".dds"]:
var candidate_path := search_dir.path_join(candidate)
if FileAccess.file_exists(candidate_path):
img_path = candidate_path
break
if img_path != "":
break
var parent_dir := search_dir.get_base_dir()
if parent_dir == search_dir:
break
search_dir = parent_dir
if not FileAccess.file_exists(img_path):
return null
var base := _load_image_file(img_path)
if base == null:
return null
if r <= l or b <= t:
return base
var at := AtlasTexture.new()
at.atlas = base
var region_top := t
at.region = Rect2(l, region_top, r - l, b - t)
return at