dsh-hoi4-modding 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """坦克设计器体检:窗口/槽位/模块/底盘槽数/背景贴图(HOI4 1.19.x)。
4
+
5
+ 只读诊断,不改任何文件。排错第一步:先跑这个,确认“界面有几个槽位位置、每个底盘有几个槽、
6
+ 槽位按钮多大、窗口背景多高”。
7
+
8
+ 用法:
9
+ python tank_designer_probe.py
10
+ python tank_designer_probe.py --game "E:/Steam/steamapps/common/Hearts of Iron IV"
11
+ python tank_designer_probe.py --mod "C:/.../my_mod"
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import re
17
+ import struct
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
22
+ try:
23
+ from _paths import find_game_dir
24
+ except Exception: # pragma: no cover
25
+ def find_game_dir():
26
+ return None
27
+
28
+
29
+ # ---------------------------------------------------------------- PDX 小工具
30
+ def read(p: Path) -> str:
31
+ return p.read_text(encoding="utf-8", errors="replace")
32
+
33
+
34
+ def depth_map(text: str) -> list[int]:
35
+ """每个字符位置所处的花括号深度(跳过注释与字符串)。"""
36
+ depths: list[int] = []
37
+ d = 0
38
+ in_str = False
39
+ i = 0
40
+ n = len(text)
41
+ while i < n:
42
+ c = text[i]
43
+ if not in_str and c == "#":
44
+ while i < n and text[i] != "\n":
45
+ depths.append(d)
46
+ i += 1
47
+ continue
48
+ if c == '"':
49
+ in_str = not in_str
50
+ depths.append(d)
51
+ i += 1
52
+ continue
53
+ depths.append(d)
54
+ if not in_str:
55
+ if c == "{":
56
+ d += 1
57
+ elif c == "}":
58
+ d -= 1
59
+ i += 1
60
+ return depths
61
+
62
+
63
+ BLOCK_START = re.compile(r"(?m)^[ \t]*([A-Za-z0-9_@.\-]+)\s*=\s*\{")
64
+
65
+
66
+ def blocks_at(text: str, depths: list[int], target: int):
67
+ """产出 (name, start, open_idx, close_idx, body):深度 == target 的块。"""
68
+ out = []
69
+ for m in BLOCK_START.finditer(text):
70
+ if depths[m.start()] != target:
71
+ continue
72
+ open_idx = m.end() - 1
73
+ d = depths[open_idx]
74
+ j = open_idx
75
+ close_idx = len(text) - 1
76
+ while j < len(text):
77
+ if text[j] == "}" and depths[j] == d:
78
+ close_idx = j
79
+ break
80
+ j += 1
81
+ body = text[open_idx + 1:close_idx]
82
+ out.append((m.group(1), m.start(), open_idx, close_idx, body))
83
+ return out
84
+
85
+
86
+ def child_blocks(body: str):
87
+ """body 内第一层 name = { ... } 子块(返回 (name, body))。"""
88
+ d = depth_map(body)
89
+ return [(n, b) for (n, _s, _o, _c, b) in blocks_at(body, d, 0)]
90
+
91
+
92
+ def attr_after(text: str, name: str, attr: str, span: int = 3000):
93
+ i = text.find(f'name = "{name}"')
94
+ if i < 0:
95
+ i = text.find(f'name="{name}"')
96
+ if i < 0:
97
+ return None
98
+ m = re.search(attr + r"\s*=\s*\{([^}]*)\}", text[i:i + span])
99
+ return m.group(1) if m else None
100
+
101
+
102
+ def width_height(snippet: str | None):
103
+ if not snippet:
104
+ return None
105
+ w = re.search(r"width\s*=\s*(\d+)", snippet)
106
+ h = re.search(r"height\s*=\s*(\d+)", snippet)
107
+ return (int(w.group(1)), int(h.group(1))) if (w and h) else None
108
+
109
+
110
+ # ---------------------------------------------------------------- DDS / GFX
111
+ def dds_size(path: Path):
112
+ try:
113
+ with open(path, "rb") as f:
114
+ head = f.read(128)
115
+ if head[:4] != b"DDS ":
116
+ return None
117
+ height = struct.unpack("<I", head[12:16])[0]
118
+ width = struct.unpack("<I", head[16:20])[0]
119
+ return width, height
120
+ except Exception:
121
+ return None
122
+
123
+
124
+ def gfx_sprite_texture(game: Path, sprite_name: str):
125
+ for f in (game / "interface").glob("**/*.gfx"):
126
+ try:
127
+ t = read(f)
128
+ except Exception:
129
+ continue
130
+ m = re.search(r'name\s*=\s*"' + re.escape(sprite_name) + r'"(.{0,600}?)texturefile\s*=\s*"([^"]+)"',
131
+ t, re.S)
132
+ if m:
133
+ return game / m.group(2)
134
+ return None
135
+
136
+
137
+ # ---------------------------------------------------------------- 报告
138
+ def main() -> int:
139
+ ap = argparse.ArgumentParser(description="坦克设计器体检(只读)")
140
+ ap.add_argument("--game", default=None)
141
+ ap.add_argument("--mod", default=None)
142
+ args = ap.parse_args()
143
+
144
+ game = Path(args.game) if args.game else find_game_dir()
145
+ if not game or not (game / "interface" / "tank_designer_view.gui").exists():
146
+ print("[!] 找不到游戏目录,请用 --game 指定 Hearts of Iron IV 根目录")
147
+ return 2
148
+
149
+ tv = game / "interface" / "tank_designer_view.gui"
150
+ ed = game / "interface" / "equipmentdesignerview.gui"
151
+ text = read(tv)
152
+
153
+ print("=" * 72)
154
+ print("坦克设计器体检 @", game)
155
+ print("=" * 72)
156
+
157
+ win = width_height(attr_after(text, "tank_designer_view", "size"))
158
+ print(f"[窗口] tank_designer_view size = {win}")
159
+
160
+ mod_pos = attr_after(text, "equipment_modules", "position")
161
+ mod_size = width_height(attr_after(text, "equipment_modules", "size"))
162
+ print(f"[容器] equipment_modules pos={mod_pos.strip() if mod_pos else None} size={mod_size}")
163
+ bp_pos = attr_after(text, "equipment_preview", "position")
164
+ bp_size = width_height(attr_after(text, "equipment_preview", "size"))
165
+ print(f"[蓝图] equipment_preview pos={bp_pos.strip() if bp_pos else None} size={bp_size}")
166
+
167
+ print("[槽位位置]")
168
+ macros = dict(re.findall(r"@([A-Za-z0-9_]+)\s*=\s*(-?\d+)", text))
169
+ for m in re.finditer(r'name\s*=\s*"(pos_custom_module_slot_window_\d+)"', text):
170
+ name = m.group(1)
171
+ pm = re.search(r"position\s*=\s*\{([^}]*)\}", text[m.start():m.start() + 300])
172
+ if not pm:
173
+ continue
174
+ raw = pm.group(1)
175
+ for k, v in macros.items():
176
+ raw = raw.replace("@" + k, v)
177
+ nums = re.findall(r"-?\d+", raw)
178
+ if len(nums) >= 2:
179
+ print(f" {name:36s} x={int(nums[0]):4d} y={int(nums[1]):4d}")
180
+
181
+ if ed.exists():
182
+ m = re.search(r'name\s*=\s*"equipment_designer_module_slot_entry"(.{0,300}?)size\s*=\s*\{([^}]*)\}',
183
+ read(ed), re.S)
184
+ if m:
185
+ print(f"[模板] equipment_designer_module_slot_entry size={width_height(m.group(2))}")
186
+
187
+ bgm = re.search(r'background\s*=\s*\{[^}]*?spriteType\s*=\s*"([^"]+)"', text, re.S)
188
+ if bgm:
189
+ sprite = bgm.group(1)
190
+ tex = gfx_sprite_texture(game, sprite)
191
+ if tex and tex.exists():
192
+ size = dds_size(tex)
193
+ print(f"[背景] {sprite} -> {tex.name} {size}")
194
+ if win and size and size[0] < win[0]:
195
+ print(" [!] 背景宽度 < 窗口宽度")
196
+ if win and size and size[1] < win[1]:
197
+ print(" [!] 背景高度 < 窗口高度:内容会溢出窗口(见 references/tank-designer.md §4.3)")
198
+
199
+ hidden = []
200
+ for m in re.finditer(r'name\s*=\s*"([a-z_0-9]+)"\s*\n\s*position\s*=\s*\{\s*x\s*=\s*(-4000|-5000)', text):
201
+ hidden.append(m.group(1))
202
+ if hidden:
203
+ print(f"[隐藏] {len(hidden)} 个元素被移出屏幕:{', '.join(hidden[:12])}"
204
+ + (" ..." if len(hidden) > 12 else ""))
205
+
206
+ # ---- 模块(游戏 + mod 覆盖)
207
+ mod_dirs = []
208
+ if args.mod:
209
+ mod_dirs.append(Path(args.mod) / "common" / "units" / "equipment" / "modules")
210
+ mod_dirs.append(game / "common" / "units" / "equipment" / "modules")
211
+ cats: dict[str, int] = {}
212
+ module_count = 0
213
+ module_ids: set[str] = set()
214
+ for d in mod_dirs:
215
+ if not d.exists():
216
+ continue
217
+ for f in d.glob("*.txt"):
218
+ t = read(f)
219
+ dm = depth_map(t)
220
+ for name, _s, _o, _c, body in blocks_at(t, dm, 0):
221
+ if name != "equipment_modules":
222
+ continue
223
+ for mod_id, mod_body in child_blocks(body):
224
+ if mod_id in ("limit",):
225
+ continue
226
+ module_count += 1
227
+ module_ids.add(mod_id)
228
+ cm = re.search(r"category\s*=\s*([A-Za-z0-9_]+)", mod_body)
229
+ if cm:
230
+ cats[cm.group(1)] = cats.get(cm.group(1), 0) + 1
231
+ print(f"[模块] {len(module_ids)} 个模块(含重复 id),{len(cats)} 个类别")
232
+ for c in sorted(cats):
233
+ print(f" {c:34s} {cats[c]:3d} 个模块")
234
+
235
+ # ---- 底盘槽数
236
+ chassis_files = []
237
+ if args.mod:
238
+ mp = Path(args.mod) / "common" / "units" / "equipment" / "tank_chassis.txt"
239
+ if mp.exists():
240
+ chassis_files.append(("mod", mp))
241
+ gp = game / "common" / "units" / "equipment" / "tank_chassis.txt"
242
+ if gp.exists():
243
+ chassis_files.append(("game", gp))
244
+ for label, cf in chassis_files:
245
+ t = read(cf)
246
+ dm = depth_map(t)
247
+ rows = []
248
+ for name, _s, _o, _c, body in blocks_at(t, dm, 1) + blocks_at(t, dm, 0):
249
+ if "is_archetype = yes" not in body:
250
+ continue
251
+ ms = re.search(r"module_slots\s*=\s*\{", body)
252
+ if not ms:
253
+ rows.append((name, 0, []))
254
+ continue
255
+ i = body.find("{", ms.start())
256
+ d = 0
257
+ j = i
258
+ while j < len(body):
259
+ if body[j] == "{":
260
+ d += 1
261
+ elif body[j] == "}":
262
+ d -= 1
263
+ if d == 0:
264
+ break
265
+ j += 1
266
+ slots = [n for n, _b in child_blocks(body[i + 1:j])]
267
+ rows.append((name, len(slots), slots))
268
+ if rows:
269
+ print(f"[底盘-{label}] archetype 槽位:")
270
+ for name, n, slots in rows:
271
+ print(f" {name:32s} {n:2d} 槽" + (f": {', '.join(slots)}" if slots else ""))
272
+
273
+ print("-" * 72)
274
+ npos = len(re.findall(r'name\s*=\s*"pos_custom_module_slot_window_\d+"', text))
275
+ print(f"[结论] 设计器槽位位置数 = {npos};槽位按钮数量由底盘 module_slots 决定,第 N 个槽 → 第 N 个位置。")
276
+ return 0
277
+
278
+
279
+ if __name__ == "__main__":
280
+ raise SystemExit(main())
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """科技卡图标导入:照片/矢量图 → 173×77 DDS(去背 + 安全区 + 卡片预览)。
4
+
5
+ 针对 HOI4 科技卡的几何:卡 183×84、Icon 控件中心 x=74 → 图标内容需左留 20、右留 4(默认)。
6
+ 支持透明 PNG(最佳)、白底、8px 棋盘格(白 + 灰)三种常见输入。
7
+
8
+ 用法:
9
+ # 单张(透明 PNG 或白底照片)
10
+ python tech_icon_import.py "虎式E型.png" out/GER_tiger_e.dds
11
+
12
+ # 批量目录 + 别名表
13
+ python tech_icon_import.py ./矢量图 out/ --alias alias.json --card-preview preview.png
14
+
15
+ # 自定义尺寸(例如装备 2D 图标 89×38 时)
16
+ python tech_icon_import.py in.png out.dds --width 89 --height 38 --margin-left 4 --margin-right 4
17
+
18
+ 依赖:Pillow;DDS 写出调用同目录 dds_helper.py(raw32)。
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import re
25
+ import subprocess
26
+ import sys
27
+ from collections import deque
28
+ from pathlib import Path
29
+
30
+ try:
31
+ from PIL import Image, ImageDraw, ImageFilter
32
+ except ImportError: # pragma: no cover
33
+ raise SystemExit("需要 Pillow:pip install pillow")
34
+
35
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
36
+ try:
37
+ from _paths import find_game_dir
38
+ except Exception: # pragma: no cover
39
+ def find_game_dir():
40
+ return None
41
+
42
+ DDS_HELPER = Path(__file__).resolve().parent / "dds_helper.py"
43
+
44
+
45
+ # ---------------------------------------------------------------- 去背
46
+ def border_pixels(im: Image.Image):
47
+ w, h = im.size
48
+ px = im.load()
49
+ for x in range(w):
50
+ yield px[x, 0]
51
+ yield px[x, h - 1]
52
+ for y in range(h):
53
+ yield px[0, y]
54
+ yield px[w - 1, y]
55
+
56
+
57
+ def bg_like(r: int, g: int, b: int, mode: str) -> bool:
58
+ mx, mn = max(r, g, b), min(r, g, b)
59
+ if mx - mn > 14: # 有明显颜色 → 不是背景
60
+ return False
61
+ v = (r + g + b) // 3
62
+ if mode == "white":
63
+ return v >= 235
64
+ if mode == "checker": # 棋盘格:白 + 灰(130/192)
65
+ return v >= 235 or 110 <= v <= 210
66
+ return v >= 235
67
+
68
+
69
+ def detect_mode(im: Image.Image) -> str:
70
+ """auto:有透明通道用 alpha;否则看边框颜色判断白底/棋盘格。"""
71
+ if im.mode == "RGBA" and im.getchannel("A").getextrema()[0] < 250:
72
+ return "alpha"
73
+ counts = {"white": 0, "checker": 0, "other": 0}
74
+ for r, g, b, _a in border_pixels(im):
75
+ mx, mn = max(r, g, b), min(r, g, b)
76
+ v = (r + g + b) // 3
77
+ if mx - mn <= 14 and v >= 235:
78
+ counts["white"] += 1
79
+ elif mx - mn <= 14 and 110 <= v <= 210:
80
+ counts["checker"] += 1
81
+ else:
82
+ counts["other"] += 1
83
+ if counts["white"] >= counts["other"] * 3:
84
+ return "white"
85
+ return "checker"
86
+
87
+
88
+ def remove_background(im: Image.Image, mode: str):
89
+ """返回 (前景 alpha mask L, 背景已透明的 RGBA)。"""
90
+ im = im.convert("RGBA")
91
+ if mode == "alpha":
92
+ return im.getchannel("A"), im
93
+
94
+ # 1) 背景候选 mask:bg-like=255
95
+ w, h = im.size
96
+ px = im.load()
97
+ mask = Image.new("L", (w, h), 0)
98
+ mp = mask.load()
99
+ for y in range(h):
100
+ for x in range(w):
101
+ r, g, b, a = px[x, y]
102
+ if bg_like(r, g, b, mode):
103
+ mp[x, y] = 255
104
+
105
+ # 2) 从边界 flood fill(128 = 已确认背景)
106
+ draw = ImageDraw.Draw(mask)
107
+ seeds = [(x, 0) for x in range(w)] + [(x, h - 1) for x in range(w)] + \
108
+ [(0, y) for y in range(h)] + [(w - 1, y) for y in range(h)]
109
+ for s in seeds:
110
+ if mp[s] == 255:
111
+ draw.floodfill(mask, s, 128, thresh=0)
112
+
113
+ # 3) 生成前景 alpha:背景=0,其余=255;内部洞按“白涂装保护”规则处理
114
+ fg = Image.new("L", (w, h), 0)
115
+ fp = fg.load()
116
+ holes = 0
117
+ for y in range(h):
118
+ for x in range(w):
119
+ v = mp[x, y]
120
+ if v == 128:
121
+ continue
122
+ if v == 255: # 内部洞(被前景包围的背景色)
123
+ r, g, b, _a = px[x, y]
124
+ rel_y = y / max(1, h - 1)
125
+ if (r + g + b) / 3 >= 235 and rel_y < 0.8:
126
+ fp[x, y] = 255 # 白色涂装/编号:保留
127
+ else:
128
+ holes += 1 # 其余洞留透明(履带缝等)
129
+ continue
130
+ fp[x, y] = 255
131
+
132
+ # 4) 腐蚀 1px 去白边
133
+ fg = fg.filter(ImageFilter.MinFilter(3))
134
+ rgba = im.copy()
135
+ rgba.putalpha(fg)
136
+ return fg, rgba
137
+
138
+
139
+ # ---------------------------------------------------------------- 安全区缩放
140
+ def resize_rgba(im: Image.Image, size: tuple[int, int]) -> Image.Image:
141
+ """RGB 与 alpha 分别 LANCZOS 缩放(Pillow 无预乘,照片缩放边缘可能有轻微灰边)。"""
142
+ r, g, b, a = im.split()
143
+ rgb = Image.merge("RGB", (r, g, b)).resize(size, Image.LANCZOS)
144
+ a2 = a.resize(size, Image.LANCZOS)
145
+ return Image.merge("RGBA", (*rgb.split(), a2))
146
+
147
+
148
+ def place_with_margins(im: Image.Image, out_w: int, out_h: int,
149
+ margin_left: int, margin_right: int, margin_y: int) -> Image.Image:
150
+ bbox = im.getchannel("A").getbbox()
151
+ if not bbox:
152
+ return Image.new("RGBA", (out_w, out_h))
153
+ im = im.crop(bbox)
154
+ avail_w = max(1, out_w - margin_left - margin_right)
155
+ avail_h = max(1, out_h - 2 * margin_y)
156
+ scale = min(avail_w / im.width, avail_h / im.height)
157
+ new = max(1, int(round(im.width * scale))), max(1, int(round(im.height * scale)))
158
+ im = resize_rgba(im, new)
159
+ canvas = Image.new("RGBA", (out_w, out_h), (0, 0, 0, 0))
160
+ x = margin_left + (avail_w - new[0]) // 2
161
+ y = margin_y + (avail_h - new[1]) // 2
162
+ canvas.alpha_composite(im, (x, y))
163
+ return canvas
164
+
165
+
166
+ # ---------------------------------------------------------------- 卡片预览
167
+ def card_preview(icon: Image.Image, out: Path, game: Path | None, card_bg: str | None = None):
168
+ bg = None
169
+ if card_bg:
170
+ bg = Path(card_bg)
171
+ elif game:
172
+ c = game / "gfx/interface/techtree/technology_available_item_bg.dds"
173
+ if c.exists():
174
+ bg = c
175
+ if not bg or not bg.exists():
176
+ print("[!] 找不到卡片背景,跳过 --card-preview")
177
+ return
178
+ card = Image.open(bg).convert("RGBA").resize((183, 84), Image.LANCZOS)
179
+ card.alpha_composite(icon, (74 - icon.width // 2, 50 - icon.height // 2))
180
+ out.parent.mkdir(parents=True, exist_ok=True)
181
+ card.save(out)
182
+ print("[preview]", out)
183
+
184
+
185
+ # ---------------------------------------------------------------- 主流程
186
+ def convert_one(src: Path, dst: Path, args, game) -> bool:
187
+ try:
188
+ im = Image.open(src).convert("RGBA")
189
+ except Exception as e:
190
+ print(f"[!] 读图失败 {src}: {e}")
191
+ return False
192
+ mode = args.mode
193
+ if mode == "auto":
194
+ mode = detect_mode(im)
195
+ if mode == "none":
196
+ alpha = im.getchannel("A")
197
+ rgba = im
198
+ else:
199
+ alpha, rgba = remove_background(im, mode)
200
+ icon = place_with_margins(rgba, args.width, args.height,
201
+ args.margin_left, args.margin_right, args.margin_y)
202
+ png = dst.with_suffix(".png")
203
+ dst.parent.mkdir(parents=True, exist_ok=True)
204
+ icon.save(png)
205
+ env = None
206
+ r = subprocess.run([sys.executable, str(DDS_HELPER), "make", str(png), str(dst),
207
+ "--width", str(args.width), "--height", str(args.height)],
208
+ capture_output=True, text=True, encoding="utf-8", errors="replace", env=env)
209
+ if r.returncode != 0:
210
+ print(f"[!] DDS 生成失败 {dst}: {(r.stderr or r.stdout or '').strip()[:200]}")
211
+ return False
212
+ if args.card_preview:
213
+ card_preview(icon, Path(args.card_preview), game, args.card_bg)
214
+ bbox = icon.getchannel("A").getbbox() or (0, 0, 0, 0)
215
+ print(f"[ok] {src.name} -> {dst.name} mode={mode} bbox={bbox}")
216
+ return True
217
+
218
+
219
+ def main() -> int:
220
+ ap = argparse.ArgumentParser(description="科技卡图标导入(照片/矢量 → 173×77 DDS)")
221
+ ap.add_argument("input", help="图片文件或目录")
222
+ ap.add_argument("output", help="输出 .dds 路径;输入为目录时为输出目录")
223
+ ap.add_argument("--mode", default="auto", choices=["auto", "alpha", "white", "checker", "none"])
224
+ ap.add_argument("--width", type=int, default=173)
225
+ ap.add_argument("--height", type=int, default=77)
226
+ ap.add_argument("--margin-left", type=int, default=20)
227
+ ap.add_argument("--margin-right", type=int, default=4)
228
+ ap.add_argument("--margin-y", type=int, default=2)
229
+ ap.add_argument("--card-preview", default=None, help="输出一张 183×84 合成卡预览 PNG")
230
+ ap.add_argument("--card-bg", default=None, help="卡片背景 DDS(默认从游戏目录找)")
231
+ ap.add_argument("--alias", default=None, help="JSON:文件名 stem / 中文名 → 科技 id")
232
+ ap.add_argument("--game", default=None)
233
+ args = ap.parse_args()
234
+
235
+ game = Path(args.game) if args.game else find_game_dir()
236
+ src = Path(args.input)
237
+ dst = Path(args.output)
238
+ aliases = {}
239
+ if args.alias:
240
+ aliases = json.loads(Path(args.alias).read_text(encoding="utf-8"))
241
+
242
+ if src.is_dir():
243
+ dst.mkdir(parents=True, exist_ok=True)
244
+ exts = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
245
+ files = [p for p in sorted(src.rglob("*")) if p.suffix.lower() in exts]
246
+ if not files:
247
+ print("[!] 目录里没有图片")
248
+ return 1
249
+ ok = 0
250
+ for f in files:
251
+ name = aliases.get(f.stem, aliases.get(f.name, f.stem))
252
+ out = dst / f"{name}.dds"
253
+ if convert_one(f, out, args, game):
254
+ ok += 1
255
+ print(f"[done] {ok}/{len(files)}")
256
+ return 0 if ok else 1
257
+
258
+ if src.is_file():
259
+ name = aliases.get(src.stem, aliases.get(src.name, dst.stem))
260
+ if dst.suffix.lower() != ".dds":
261
+ dst = dst / f"{name}.dds"
262
+ return 0 if convert_one(src, dst, args, game) else 1
263
+
264
+ print("[!] 输入不存在:", src)
265
+ return 1
266
+
267
+
268
+ if __name__ == "__main__":
269
+ raise SystemExit(main())