Initial commit: RE tooling and docs for The I of the Dragon localization/HD

This commit is contained in:
DragonHD
2026-09-20 09:57:07 +08:00
commit caf7376019
26 changed files with 1624 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
import argparse, os, csv, json, struct, collections
from PIL import Image
Image.MAX_IMAGE_PIXELS = None
def tga_header(b):
return {"w": struct.unpack_from("<H", b, 12)[0],
"h": struct.unpack_from("<H", b, 14)[0],
"bpp": b[16], "type": b[2]}
def dds_header(b):
h = struct.unpack_from("<I", b, 12)[0]
w = struct.unpack_from("<I", b, 16)[0]
mip = struct.unpack_from("<I", b, 28)[0]
four = b[84:88].decode("latin1").strip()
return {"w": w, "h": h, "fourcc": four, "mip": mip}
def classify(name):
low = name.lower().replace("\\", "/")
flags = []
if any(k in low for k in ("shrift", "debugfont")) or low.endswith("stats/digits.tga"):
flags.append("font_atlas")
if "overhead" in low or "mainmenu" in low:
flags.append("ui_atlas")
if any(k in low for k in ("menu", "credits", "tutorial", "game-name", "button-")):
flags.append("text_baked")
if low.startswith(("landscape/", "sky/", "waterfall")) or "cloud" in low:
flags.append("tiling_likely")
if "spells" in low or "/fx/" in low or low.startswith("fx/"):
flags.append("fx")
group = low.split("/")[0] if "/" in low else "(root)"
return group, ";".join(flags)
def main():
ap = argparse.ArgumentParser(description="Convert Dragon textures to PNG and build an upscale-planning report.")
ap.add_argument("--csv", required=True, help="index csv produced by res_unpack.py")
ap.add_argument("--indir", required=True, help="extracted original directory")
ap.add_argument("--outdir", required=True, help="PNG output directory")
ap.add_argument("--report", required=True, help="output report csv")
args = ap.parse_args()
rows = list(csv.DictReader(open(args.csv, newline="", encoding="utf-8")))
out_rows = []
groups = collections.Counter()
flags_count = collections.Counter()
fmt_count = collections.Counter()
total_px = 0
failed = []
for i, r in enumerate(rows):
name = r["name"]
src = os.path.join(args.indir, r["file"])
ext = os.path.splitext(name)[1].lower()
rec = {"name": name, "ext": ext, "src": r["file"], "png": "", "orig_w": "", "orig_h": "",
"mode": "", "has_alpha": "", "dds_fourcc": "", "dds_mips": "",
"group": "", "flags": "", "orig_bytes": r["size"]}
group, flags = classify(name)
rec["group"] = group
rec["flags"] = flags
for f in flags.split(";"):
if f:
flags_count[f] += 1
groups[group] += 1
try:
if ext in (".tga", ".dds"):
raw = open(src, "rb").read()
if ext == ".tga":
hi = tga_header(raw)
fmt_count["TGA%s/%dbpp" % (hi["type"], hi["bpp"])] += 1
rec["orig_w"], rec["orig_h"] = hi["w"], hi["h"]
else:
hi = dds_header(raw)
fmt_count["DDS:" + hi["fourcc"]] += 1
rec["orig_w"], rec["orig_h"] = hi["w"], hi["h"]
rec["dds_fourcc"], rec["dds_mips"] = hi["fourcc"], hi["mip"]
im = Image.open(src)
im.load()
if im.mode == "P":
im = im.convert("RGBA")
rec["mode"] = im.mode
alpha = im.mode in ("RGBA", "LA")
if alpha:
a = im.getchannel("A") if im.mode == "RGBA" else im.getchannel("A")
alpha = a.getextrema()[0] < 255
rec["has_alpha"] = int(bool(alpha))
total_px += im.size[0] * im.size[1]
png_path = os.path.join(args.outdir, os.path.splitext(r["file"])[0] + ".png")
os.makedirs(os.path.dirname(png_path), exist_ok=True)
im.save(png_path)
rec["png"] = os.path.relpath(png_path, args.outdir)
else:
fmt_count["other"] += 1
rec["flags"] = (flags + ";non_texture").strip(";")
except Exception as e:
failed.append((name, str(e)))
rec["mode"] = "ERROR:" + str(e)[:60]
out_rows.append(rec)
if (i + 1) % 100 == 0:
print("processed %d/%d" % (i + 1, len(rows)))
with open(args.report, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=list(out_rows[0].keys()))
w.writeheader()
w.writerows(out_rows)
summary = {
"total": len(out_rows),
"formats": dict(fmt_count),
"groups": dict(groups.most_common()),
"flags": dict(flags_count),
"total_megapixels": round(total_px / 1e6, 2),
"failed": failed,
}
print(json.dumps(summary, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()