# UiAssets (P1) —— 解析 uiscript 里的图片路径(`.sub` / `.tga` / `.png` / `.jpg` / `.dds`)。 # # 路径形如 `d:/ymir work/ui/public/middle_button_01.sub`。解析: # 1) 优先从 40250 Client/pack(Metin2Pack)取(保持 d:/ 盘符或规范相对路径) # 2) 支持 pack 内 .sub 子图切片(AtlasTexture),支持 version 1.0 / 2.0 及公用 Public.dds / IntroEmpire.dds 母图寻址 # 3) 若 pack 找不到且指定了 assets_root,回退到散文件目录 # 4) .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 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 == "": return null var key := (assets_root if assets_root != "" else "pack") + "|" + vpath if _cache.has(key): return _cache[key] var tex: Texture2D = _load_pack_any(vpath) if tex == null and assets_root != "": tex = _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] var tex: Texture2D = _load_pack_any(vpath) _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 _find_pack_path(vpath: String) -> String: if not ClassDB.class_exists("Metin2Pack") or not Metin2Pack.is_ready(): return "" var norm := vpath.replace("\\", "/").strip_edges() var stripped := _strip_drive(norm) var candidates: Array[String] = [ norm, stripped, "d:/" + stripped, norm.to_lower(), stripped.to_lower(), ("d:/" + stripped).to_lower(), ] for c in candidates: if Metin2Pack.exists(c): return c return "" static func _load_pack_any(vpath: String) -> Texture2D: var pack_path := _find_pack_path(vpath) if pack_path == "": return null var ext := pack_path.get_extension().to_lower() if ext == "sub": return _load_pack_sub(pack_path) return _load_pack_image(pack_path) static func _load_pack_sub(pack_path: String) -> Texture2D: if not ClassDB.class_exists("Metin2Pack") or not Metin2Pack.is_ready(): return null if not Metin2Pack.exists(pack_path): return null var bytes: PackedByteArray = Metin2Pack.get_bytes(pack_path) if bytes.is_empty(): return null var txt := bytes.get_string_from_ascii() var image_name := "" var version := "" 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].to_lower(): "version": version = parts[1].strip_edges().trim_prefix('"').trim_suffix('"') "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 var sub_dir := pack_path.get_base_dir() var candidates: Array[String] = [] if version == "2.0": candidates.append(sub_dir.path_join(image_name)) candidates.append("d:/ymir work/ui/".path_join(image_name)) else: candidates.append("d:/ymir work/ui/".path_join(image_name)) candidates.append(sub_dir.path_join(image_name)) var search_dir := sub_dir for _i in 4: var parent := search_dir.get_base_dir() if parent == search_dir or parent.is_empty(): break candidates.append(parent.path_join(image_name)) search_dir = parent var base_tex: Texture2D = null for cand in candidates: base_tex = _load_pack_any(cand) if base_tex != null: break if base_tex == null: return null if r <= l or b <= t: return base_tex var at := AtlasTexture.new() at.atlas = base_tex at.region = Rect2(l, t, r - l, b - t) return at static func _resolve(assets_root: String, rel: String) -> String: var direct := assets_root.path_join(rel) if FileAccess.file_exists(direct): return direct # 散包:// 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 return "" static func _load_uncached(assets_root: String, vpath: String) -> Texture2D: var rel := _strip_drive(vpath) var real := _resolve(assets_root, rel) if real == "": for ext: String in [".tga", ".png", ".jpg"]: real = _resolve(assets_root, rel.get_basename() + ext) if real != "": break if real == "": return _load_pack_any(vpath) 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: 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 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