# UiAssets (P1) —— 解析 uiscript 里的图片路径(`.sub` / `.tga` / `.png` / `.jpg`)。 # # 路径形如 `d:/ymir work/ui/public/middle_button_01.sub`。解析: # 1) 去掉盘符前缀 # 2) 依次试: / · //(散包目录) # 3) `.sub` = 子图描述(title/image/left/top/right/bottom)→ 载入其 image + 裁剪成 AtlasTexture # `.dds` 走 Metin2World.load_dds(C++ dxt 解码)运行时解成 Image。 extends RefCounted static var _cache := {} static var _dds_helper: Object = null # 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 if _dds_helper == null or not is_instance_valid(_dds_helper): _dds_helper = ClassDB.instantiate("Metin2World") var img = _dds_helper.call("load_dds", path) 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 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 # 散包:// 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 # 大小写不敏感兜底:/**/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 null if real.get_extension().to_lower() == "sub": return _load_sub(real) return _load_image_file(real) 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/, 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 at.region = Rect2(l, t, r - l, b - t) return at