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
+27
View File
@@ -0,0 +1,27 @@
# Ignore everything at the top level except our own code and docs.
# Original game assets, cracks, extracted/extracted-derived binaries and
# generated reports are all regenerable (or not ours) and must not be tracked.
/*
!/tools
!/docs
!/.gitignore
!/README.md
# Belt-and-suspenders: never track game / crack / media / build artifacts
*.res
*.iso
*.avi
*.wav
*.mp3
*.tga
*.dds
*.png
*.bin
*.exe
*.dll
*.iso
/crack/
/original/
/png/
/report/
/patch/
+35
View File
@@ -0,0 +1,35 @@
# The I of the Dragon — fan tools (localization + HD)
Reverse-engineering and localization tooling for *The I of the Dragon* (Topware / Primal Software).
This repository tracks **only our own tools and documentation**. It does **not** contain the game,
its assets, or any cracked distribution — those are regenerated/produced locally from a legit copy.
## Layout
```
tools/ Python tooling (unpack, repack, preprocess, font patch, decoders)
docs/ DEVLOG, format specs, patch notes
```
Working data (not tracked): `original/`, `png/`, `report/`, `patch/`, `crack/`.
## Tooling
| script | purpose |
|---|---|
| `tools/res_unpack.py` | extract a `.res` archive (index + raw blobs, 16-byte aligned) |
| `tools/res_repack.py` | rebuild a `.res` byte-identically from a directory |
| `tools/preprocess.py` | convert textures to PNG + build classification report |
| `tools/make_plan.py` | build an HD upscale plan from the report |
| `tools/decode_dat.py` | decode/encode the XOR-obfuscated `Fonts.dat` / `*Description.dat` |
| `tools/fontparse.py` | parse `Fonts.dat` into font blocks and glyph rects |
| `tools/add_section.py` | add an executable section to the game PE |
| `tools/apply_cjk.py` | apply the double-byte font-rendering patch |
| `tools/xrefs.py` | find call/jmp xrefs in the game executable |
See `docs/` for the reverse-engineered formats and addresses.
## Legal
No game data is distributed here. Patches are produced by the user against their own copy.
+54
View File
@@ -0,0 +1,54 @@
# DEVLOG
Working log for the *The I of the Dragon* localization / HD project.
Newest entries on top.
## 2026-09-20 — Double-byte font patch proven
- Reverse-engineered the whole text pipeline (see `patch-notes.md`).
- Built `tools/add_section.py` (PE section injection) and verified the patched exe still runs.
- Built `tools/apply_cjk.py`: assembles two code caves with keystone, adds a `.cjk` section,
hooks the draw loop (`0x4EACB0`) and the metrics loop (`0x4E8026`).
- **Verified on screen**: a GB2312-coded string `0xC1FA` (龙) in a menu label rendered a 16x16
glyph at the mapped atlas position (green control test). Double-byte decode + render works.
- Game files restored afterwards (exe SHA1 `9EDB9B99...`, `Textures.res` byte-identical).
### Next
1. Big atlas (2048x2048): ASCII glyphs (copied) + CJK glyphs; re-point `Fonts.dat` `Texture` + ASCII rects.
2. Generator for `cjk_glyph` / `cjk_metric` tables from a chosen charset.
3. Convert `Strings.dat`, scripts, and `*Description.dat` display text to GB2312.
4. Redraw baked-in text textures (menu buttons, credits, tutorial).
5. Package: patched exe + atlas + data files.
## 2026-09-20 — HD texture preprocessing
- Cracked the `.res` archive format and built `res_unpack.py` / `res_repack.py`
(byte-identical round-trip once 16-byte alignment was found).
- Extracted `Textures.res` (1884) and `Geometry.res` (1397).
- Built `preprocess.py` (TGA/DDS→PNG, classification) and `make_plan.py`.
- Result: 63.6 Mpix, 1882 PNG, 0 failures. Plan: 1195 upscale / 499 tiling / 182 review / 8 skip
(6 font atlases + 2 non-textures).
## 2026-09-20 — Cracking the `.dat` obfuscation
- `Fonts.dat` / `UnitDescription.dat` / `AIDescription.dat` decoded:
`b"==" + XOR(plaintext, "GBDFYTNE")`; key at `0x620D18`, decoder at `0x4AE1D0`.
- `Fonts.dat` parsed: 4 font blocks, per-char atlas rects; parser keeps only the first byte of `Code`
(`0x4EC168`), so the stock engine is single-byte.
## 2026-09-20 — Initial findings
- Engine has a data-driven multi-language system: per-language folders for scripts/speech/video/UI
textures and language columns in `Strings.dat`.
- Text is externalized: `Strings.dat` (499 entries, plain) and plain-text scripts.
- First font test: replaced the `e` glyph in `Shrift_gb_Germany.TGA` and confirmed the atlas override
works in game (rendered 龙 where `e` was).
- Investigated the "pirate build": PROPHET 2009 Topware release, languages EN/DE/PL/CZ/HU, **no
Chinese**; its exe is identical to the Steam exe (no engine patch), so the Chinese build the user
remembers was a different repack (not recoverable from it).
## Decisions
- Do the localization ourselves (route B): patch the engine for double-byte text, build our own font
atlas, translate only display text (never Ids / event names / unit types) to avoid trigger bugs.
- Keep the repository text-only; regenerate binaries from scripts (see `.gitignore`).
+84
View File
@@ -0,0 +1,84 @@
# File formats
All addresses are absolute VAs in `TheIOfTheDragon.exe` (image base `0x400000`).
## 1. `Data\Misc\Strings.dat` — plain text
Same syntax as the other `.dat` files, but **not** obfuscated. Blocks look like:
```
String
{
Id = "WindowCaption"
English = "..."
Russian = "..."
German = "..."
}
```
- 499 blocks in the Steam build. Keys are `String{ Id, English, Russian, German }`
(retail multilanguage builds also carry `Polish`, `Czech`, `Hungarian`, `Hungarian/Russian` variants
split across separate installed files).
- **Translate only the language values.** Never touch `Id`.
- The file is single-byte code page text (Russian is CP1251).
## 2. `Data\Misc\Fonts.dat`, `UnitDescription.dat`, `AIDescription.dat` — XOR-obfuscated text
Layout: `b"==" + XOR(plaintext, repeating key)`.
- Magic `==` at VA `0x620D24`.
- Repeating XOR key `GBDFYTNE` (8 bytes) at VA `0x620D18`.
- Decoder/encoder routine at `0x4AE1D0`.
Decoded, these are the same text format as `Strings.dat`:
- `Fonts.dat`: `Font { Name; Language; Texture; SpaceWidth; LineDistance; Char { Code; X1;Y1;X2;Y2 } ... }`
- `UnitDescription.dat`: unit/enemy definitions (Russian comments in CP1251 + English keys).
- `AIDescription.dat`: `Brain { AI = ...; TypeAI = ...; ... }`.
Round-trip is exact; see `tools/decode_dat.py`.
## 3. `res` archives (`Textures.res`, `Geometry.res`)
```
uint32 count
repeat count:
uint32 nameLen
char[] name (backslash separated)
uint32 offset (absolute file offset)
uint32 size (exact blob size)
pad the index to 16 bytes
data region:
for each entry: write blob, then pad to 16-byte boundary
(the last blob is NOT padded)
```
- All offsets are 16-byte aligned.
- `Textures.res` holds 1884 entries: 947 TGA + 935 DDS (DXT1/DXT5) + 2 misc.
- `res_unpack.py` / `res_repack.py` round-trip is **byte identical** (verified by SHA1).
## 4. Scripts
- `Data\Scripts\<Language>\MainMission.dsc`, `Tutorial.dsc` — plain text, **loaded by the engine**
(path template `Data\Scripts\%s\...`; `.dsc` is referenced, `.csc` is not).
- Block-structured scripting language. Quoted strings are of two kinds:
- **display text** (translate): `SetMissionDescription("...")`
- **logic identifiers** (never translate): event names `EnableEvent("TimePassed")`,
`LastEvent_Type == "TimePassed"`, commands `EntityCommand("FaceThePlayer",...)`,
modes `SetGlobalAIMode("Disable")`, unit types `CreateEntity(..., "Human", ...)`, etc.
- Translating logic identifiers is what breaks mission triggers ("完成任务不触发剧情").
## 5. Language mechanism
- Current language: registry `HKCU\Software\Primal\Dragon` value `language`.
- Assets are per-language by folder: `Data\Scripts\<L>`, `Data\Sounds\Speech\<L>`,
`Data\Video\<L>`, `Data\Textures\Ui\...\<L>`.
- `Fonts.dat` / `Strings.dat` are installed per language group (observed: `[en de] [pl] [cs] [hu]`;
Steam build ships a single English variant with `English/Russian/German`).
- Loose files under `Data\Textures\...` override the packed copies.
## 6. Textures
- TGA: uncompressed 24/32 bpp and RLE 32 bpp.
- DDS: DXT1 (535) and DXT5 (400), plus dimensions from 8x8 up to 1024x1024.
- Total 63.6 Mpix across 1884 textures.
+80
View File
@@ -0,0 +1,80 @@
# Font rendering map + double-byte patch
## Font object layout (size `0x191C`, embedded by value in a manager struct)
Two UI font objects live at `manager + 0x3A8` and `manager + 0x1CC4` (difference = `0x191C`).
```
+0x00 texture pointer (D3D texture object)
+0x04 max width
+0x08 max height
+0x0C SpaceWidth
+0x10 LineDistance
+0x14 CharDistance
+0x18 flag (1 = built)
+0x1C 256 glyph entries x 16 bytes = 4 floats (u1,v1,u2,v2) normalized UV
+0x101C 256 metric entries x 8 bytes = { int advance, int height }
+0x181C 256 "defined" bytes
```
The table stride differs (16 vs 8 vs 1), which is why the tables cannot be enlarged in place and a
side table is used for CJK.
## Key code
| VA | role |
|---|---|
| `0x4EBF90` | parse/load `Fonts.dat` (generic config reader) |
| `0x4E7DC0` | set font texture |
| `0x4E7E30` | `AddChar(code, x1,y1,x2,y2)` — normalizes rect → stores glyph float[4] + metric[2] |
| `0x4EC168` | `mov cl,[ebp]` — parser takes only the **first byte** of `Code` |
| `0x4E7FD0` | measure text; loop body `0x4E7FF0`; metric lookups `0x4E802C`, `0x4E8033` |
| `0x4EAC40` | draw text; loop head |
| `0x4EACB0` | `movzx ecx,al` — glyph lookup site in draw loop |
| `0x4EACDC`,`0x4EAE9A` | metric lookups in draw loop |
| `0x4E56A0` | `IsCharDefined(font, code)` |
Texture is bound **once per draw call** (`[font]` + vtable `+0xF4`), so ASCII and CJK glyphs must
share one texture atlas.
## Patch design (implemented)
A new PE section `.cjk` holds two code caves and two big tables:
```
.cjk RVA 0x566000 (VA 0x966000), size 0x1C0000
0x00 draw_cave
0x0100 metrics_cave
0x0200 cjk_glyph : 65536 x 16 bytes (u1,v1,u2,v2 floats)
0x100200 cjk_metric : 65536 x 8 bytes (advance,height ints)
```
Hooks (5-byte `jmp rel32`):
- `0x4EACB0 -> draw_cave` (was `movzx ecx,al`)
- `0x4E8026 -> metrics_cave` (was `mov ebp,[ecx+0x14]`)
Behaviour of each cave:
- `al < 0x80`: execute the original instructions, jump back to the next instruction.
- `al >= 0x80`: decode a 2-byte code `(al<<8)|[ptr+1]`; copy the glyph float[4] into font glyph slot
`0xFF` and the metric into slot `0xFF`; set `ecx = 0xFF`, advance the string pointer by one extra
byte; jump back into the engine's unchanged quad-building code.
Slot `0xFF` glyph lives at `font+0x100C`, its metric at `font+0x1814`; these do not collide with the
real tables.
## Requirements / limits
- The font texture must contain **both** ASCII and CJK glyphs (one texture per draw).
Plan: replace the UI font `Texture` with a 2048x2048 atlas and re-point the ASCII `Char` rects.
- Lead bytes >= `0x80` are reserved for DBCS. ASCII is untouched.
- Confirmed end to end: a GB2312-coded menu string rendered the mapped glyph on screen.
## Tools
- `tools/add_section.py` — append an executable PE section.
- `tools/apply_cjk.py` — assemble the caves (keystone), add `.cjk`, write hooks and tables.
- `tools/decode_dat.py` — decode/encode the XOR `.dat` files.
- `tools/fontparse.py` — parse `Fonts.dat` into blocks + glyph rects.
- `tools/xrefs.py`, `tools/disasm.py`, `tools/scan_disp.py` — RE helpers.
+57
View File
@@ -0,0 +1,57 @@
import struct, sys, shutil, os
def align(x, a):
return (x + a - 1) // a * a
def main():
src, dst = sys.argv[1], sys.argv[2]
size = int(sys.argv[3], 0) if len(sys.argv) > 3 else 0x300000
secname = sys.argv[4] if len(sys.argv) > 4 else ".cjk"
data = bytearray(open(src, "rb").read())
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
coff = e_lfanew + 4
nsec = struct.unpack_from("<H", data, coff + 2)[0]
optsize = struct.unpack_from("<H", data, coff + 16)[0]
opt = coff + 20
imagebase = struct.unpack_from("<I", data, opt + 28)[0]
sect_align = struct.unpack_from("<I", data, opt + 32)[0]
file_align = struct.unpack_from("<I", data, opt + 36)[0]
size_image = struct.unpack_from("<I", data, opt + 56)[0]
so = opt + optsize
secs = []
for i in range(nsec):
o = so + i * 40
vs, va, rs, rp = struct.unpack_from("<IIII", data, o + 8)
secs.append((va, vs, rp, rs, o))
last_va, last_vs = secs[-1][0], secs[-1][1]
new_va = align(last_va + last_vs, sect_align)
new_raw = align(len(data), file_align)
if so + (nsec + 1) * 40 > secs[0][2]:
print("no room for new section header", file=sys.stderr)
sys.exit(3)
hdr = so + nsec * 40
name = secname.encode("ascii")[:8].ljust(8, b"\x00")
struct.pack_into("<8sIIII", data, hdr, name, size, new_va, align(size, file_align), new_raw)
struct.pack_into("<IIHH", data, hdr + 36, 0xE0000020, 0, 0, 0)
# characteristics: CNT_CODE|CNT_INITIALIZED_DATA|MEM_EXECUTE|MEM_READ|MEM_WRITE = 0xE0000020
struct.pack_into("<H", data, coff + 2, nsec + 1)
struct.pack_into("<I", data, opt + 56, align(new_va + size, sect_align))
if len(data) < new_raw:
data += b"\x00" * (new_raw - len(data))
data += b"\x00" * align(size, file_align)
open(dst, "wb").write(data)
print("added %s VA=0x%x raw=0x%x size=0x%x -> %s (%d bytes)" % (secname, new_va, new_raw, size, dst, len(data)))
print("section VA (absolute, imagebase 0x%x): 0x%x" % (imagebase, imagebase + new_va))
if __name__ == "__main__":
main()
+173
View File
@@ -0,0 +1,173 @@
import struct, sys, os
from keystone import Ks, KS_ARCH_X86, KS_MODE_32
ORIG = r"F:\steam\steamapps\common\The I of the Dragon\TheIOfTheDragon.exe"
OUT = r"E:\DragonHD\patch\TheIOfTheDragon_cjk.exe"
TBL = r"E:\DragonHD\patch\tables"
HOOK_DRAW_VA = 0x4EACB0
HOOK_MEAS_VA = 0x4E8026
BACK_DRAW_VA = 0x4EACB8
BACK_MEAS_VA = 0x4E8049
SEC_SIZE = 0x1C0000
OFF_DRAW = 0x0000
OFF_MEAS = 0x0100
OFF_GLYPH = 0x0200
OFF_METRIC = 0x100200
def align(x, a):
return (x + a - 1) // a * a
def load_tables():
g = open(os.path.join(TBL, "cjk_glyph.bin"), "rb").read()
m = open(os.path.join(TBL, "cjk_metric.bin"), "rb").read()
assert len(g) == 65536 * 16, len(g)
assert len(m) == 65536 * 8, len(m)
return g, m
def main():
data = bytearray(open(ORIG, "rb").read())
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
coff = e_lfanew + 4
nsec = struct.unpack_from("<H", data, coff + 2)[0]
optsize = struct.unpack_from("<H", data, coff + 16)[0]
opt = coff + 20
imagebase = struct.unpack_from("<I", data, opt + 28)[0]
sect_align = struct.unpack_from("<I", data, opt + 32)[0]
file_align = struct.unpack_from("<I", data, opt + 36)[0]
so = opt + optsize
secs = []
for i in range(nsec):
o = so + i * 40
vs, va, rs, rp = struct.unpack_from("<IIII", data, o + 8)
secs.append((va, vs, rp, rs, o))
last_va, last_vs = secs[-1][0], secs[-1][1]
new_va = align(last_va + last_vs, sect_align)
new_raw = align(len(data), file_align)
sec_abs = imagebase + new_va
glyph_va = sec_abs + OFF_GLYPH
metric_va = sec_abs + OFF_METRIC
draw_va = sec_abs + OFF_DRAW
meas_va = sec_abs + OFF_MEAS
ks = Ks(KS_ARCH_X86, KS_MODE_32)
draw_asm = """
cmp al, 0x80
jb ascii
movzx ecx, al
shl ecx, 8
movzx eax, byte ptr [edx + 1]
or ecx, eax
mov eax, ecx
shl eax, 4
add eax, {glyph}
mov edx, [eax]
mov [esi + 0x100C], edx
mov edx, [eax + 4]
mov [esi + 0x1010], edx
mov edx, [eax + 8]
mov [esi + 0x1014], edx
mov edx, [eax + 0xC]
mov [esi + 0x1018], edx
mov eax, ecx
shl eax, 3
add eax, {metric}
mov edx, [eax]
mov [esi + 0x1814], edx
mov edx, [eax + 4]
mov [esi + 0x1818], edx
inc dword ptr [esp + 0x10]
mov ecx, 0xFF
mov edx, 0xFF
shl edx, 4
jmp {back}
ascii:
movzx ecx, al
mov edx, ecx
shl edx, 4
jmp {back}
""".format(glyph=hex(glyph_va), metric=hex(metric_va), back=hex(BACK_DRAW_VA))
draw_code, _ = ks.asm(draw_asm, draw_va)
draw_code = bytes(draw_code)
meas_asm = """
cmp al, 0x80
jb ascii
movzx eax, al
mov ah, byte ptr [ebx + 1]
mov ebp, [ecx + 0x14]
add ebp, dword ptr [eax*8 + {metric}]
add edx, ebp
mov ebp, dword ptr [eax*8 + {metric} + 4]
cmp ebp, esi
jle skip
mov esi, ebp
skip:
mov ebp, [esp + 0x10]
inc ebx
jmp {back}
ascii:
mov ebp, [ecx + 0x14]
movzx eax, al
add ebp, dword ptr [ecx + eax*8 + 0x101c]
lea eax, [ecx + eax*8 + 0x101c]
mov eax, [eax + 4]
add edx, ebp
cmp eax, esi
mov ebp, [esp + 0x10]
jle skip2
mov esi, eax
skip2:
jmp {back}
""".format(metric=hex(metric_va), back=hex(BACK_MEAS_VA))
meas_code, _ = ks.asm(meas_asm, meas_va)
meas_code = bytes(meas_code)
print("draw_cave %d bytes @0x%x, metrics_cave %d bytes @0x%x" % (len(draw_code), draw_va, len(meas_code), meas_va))
if so + (nsec + 1) * 40 > secs[0][2]:
print("no room for section header", file=sys.stderr)
sys.exit(3)
# add section header
hdr = so + nsec * 40
struct.pack_into("<8sIIII", data, hdr, b".cjk".ljust(8, b"\x00"), SEC_SIZE, new_va, align(SEC_SIZE, file_align), new_raw)
struct.pack_into("<IIHH", data, hdr + 36, 0xE0000020, 0, 0, 0)
struct.pack_into("<H", data, coff + 2, nsec + 1)
struct.pack_into("<I", data, opt + 56, align(new_va + SEC_SIZE, sect_align))
if len(data) < new_raw:
data += b"\x00" * (new_raw - len(data))
data += b"\x00" * align(SEC_SIZE, file_align)
sec_off = new_raw
data[sec_off + OFF_DRAW: sec_off + OFF_DRAW + len(draw_code)] = draw_code
data[sec_off + OFF_MEAS: sec_off + OFF_MEAS + len(meas_code)] = meas_code
glyph, metric = load_tables()
data[sec_off + OFF_GLYPH: sec_off + OFF_GLYPH + len(glyph)] = glyph
data[sec_off + OFF_METRIC: sec_off + OFF_METRIC + len(metric)] = metric
def hook(va, target):
off = va - imagebase
# .text: raw==va for this exe (.text rp=0x1000, va=0x1000)
foff = off
code, _ = ks.asm("jmp %s" % hex(target), va)
code = bytes(code)
assert len(code) == 5, code
data[foff:foff+5] = code
print("hook @0x%x -> 0x%x" % (va, target))
hook(HOOK_DRAW_VA, draw_va)
hook(HOOK_MEAS_VA, meas_va)
os.makedirs(os.path.dirname(OUT), exist_ok=True)
open(OUT, "wb").write(data)
print("wrote", OUT, len(data), "bytes")
if __name__ == "__main__":
main()
+62
View File
@@ -0,0 +1,62 @@
import os, sys, collections, math
base = r"F:\steam\steamapps\common\The I of the Dragon\Data\Misc"
files = ["Fonts.dat", "UnitDescription.dat", "AIDescription.dat"]
def printable_ratio(b):
if not b: return 0
return sum(1 for x in b if 32 <= x < 127) / len(b)
def score_text(b):
# rough score: printable ratio + bonus for common english words
r = printable_ratio(b)
sample = bytes(b)
kw = 0
for w in [b"Char", b"Code", b"Font", b"SpaceWidth", b".tga", b"Shrift", b"Distance", b"Id", b"String"]:
kw += sample.count(w)
return r, kw
for f in files:
p = os.path.join(base, f)
data = open(p, "rb").read()
print("="*70)
print(f, "size", len(data))
print("first 32:", data[:32].hex(" "))
# 1) single-byte xor
best = []
for k in range(256):
dec = bytes(x ^ k for x in data)
r, kw = score_text(dec)
best.append((kw, r, k, dec[:80]))
best.sort(key=lambda t: (-t[0], -t[1]))
print("-- best single-byte XOR by keyword --")
for kw, r, k, s in best[:5]:
print(f" k=0x{k:02X} kw={kw} pr={r:.3f} {s!r}")
# 2) additive / subtractive
best = []
for k in range(256):
dec = bytes((x - k) & 0xFF for x in data)
r, kw = score_text(dec)
best.append((kw, r, k, dec[:80]))
best.sort(key=lambda t: (-t[0], -t[1]))
print("-- best subtract key --")
for kw, r, k, s in best[:3]:
print(f" k=0x{k:02X} kw={kw} pr={r:.3f} {s!r}")
# 3) repeating-key XOR, key length 2..64, derive key from space frequency
print("-- repeating-key XOR (key from most-common byte == space) --")
results = []
for L in range(2, 65):
key = bytearray()
for r in range(L):
col = data[r::L]
c = collections.Counter(col).most_common(1)[0][0]
key.append(c ^ 0x20)
dec = bytes(x ^ key[i % L] for i, x in enumerate(data))
rr, kw = score_text(dec)
results.append((kw, rr, L, bytes(key), dec[:80]))
results.sort(key=lambda t: (-t[0], -t[1]))
for kw, rr, L, key, s in results[:6]:
print(f" L={L:2d} key={key.hex()} kw={kw} pr={rr:.3f} {s!r}")
+23
View File
@@ -0,0 +1,23 @@
import os
base = r"F:\steam\steamapps\common\The I of the Dragon\Data\Misc"
out = r"C:\Users\29452\AppData\Local\Temp\opencode\decoded"
os.makedirs(out, exist_ok=True)
key = b"GBDFYTNE"
for f in ["Fonts.dat", "UnitDescription.dat", "AIDescription.dat"]:
d = open(os.path.join(base, f), "rb").read()
assert d[:2] == b"==", d[:2]
body = d[2:]
dec = bytes(body[i] ^ key[i % len(key)] for i in range(len(body)))
fp = os.path.join(out, f + ".txt")
open(fp, "wb").write(dec)
print(f, "->", fp, len(dec), "bytes")
# also prove re-encode round trips
def enc(text_bytes):
return b"==" + bytes(text_bytes[i] ^ key[i % len(key)] for i in range(len(text_bytes)))
for f in ["Fonts.dat", "UnitDescription.dat", "AIDescription.dat"]:
d = open(os.path.join(base, f), "rb").read()
body = d[2:]
dec = bytes(body[i] ^ key[i % len(key)] for i in range(len(body)))
assert enc(dec) == d, f
print("round-trip OK")
+46
View File
@@ -0,0 +1,46 @@
import struct, sys
from capstone import *
exe = r"F:\steam\steamapps\common\The I of the Dragon\TheIOfTheDragon.exe"
data = open(exe, "rb").read()
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
coff = e_lfanew + 4
machine, nsec, tstamp, symptr, nsym, optsize, chars = struct.unpack_from("<HHIIIHH", data, coff)
opt = coff + 20
imagebase = struct.unpack_from("<I", data, opt+28)[0]
sec_off = opt + optsize
sections = []
for i in range(nsec):
o = sec_off + i*40
name = data[o:o+8].rstrip(b"\x00").decode("latin1")
vsize, vaddr, rawsize, rawptr = struct.unpack_from("<IIII", data, o+8)
sections.append((name, vaddr, vsize, rawptr, rawsize))
def va2off(va):
rva = va - imagebase
for name, sva, vs, rp, rs in sections:
if sva <= rva < sva+max(vs, rs):
return rp + (rva-sva)
return None
md = Cs(CS_ARCH_X86, CS_MODE_32)
md.detail = True
def show(fileoff, n=60, before=6):
start = fileoff - before
# linear disasm from section start is messy; assume instruction boundary is ok-ish
code = data[start:start+ n*8]
# compute VA
rva = None
for name, sva, vs, rp, rs in sections:
if rp <= start < rp+rs:
rva = sva + (start-rp); base = imagebase
addr = imagebase + rva
print(f"### disasm at file {start:#x} VA {addr:#x}")
for ins in md.disasm(code, addr):
print(f" {ins.address:08x}: {ins.mnemonic:8s} {ins.op_str}")
import sys
args = sys.argv[1:]
for a in args:
show(int(a,16))
+28
View File
@@ -0,0 +1,28 @@
from PIL import Image
import sys
p = sys.argv[1]
img = Image.open(p).convert("L")
W, H = img.size
px = img.load()
# edge density per row: count |v(x)-v(x-2)| > 40
rows = []
for y in range(H):
c = 0
for x in range(2, W):
if abs(px[x, y] - px[x-2, y]) > 45:
c += 1
rows.append(c)
# report top rows grouped
thr = max(20, sorted(rows)[int(H*0.97)])
print("W,H", W, H, "row-edge threshold", thr)
bands = []
y = 0
while y < H:
if rows[y] > thr:
y0 = y
while y < H and rows[y] > thr:
y += 1
bands.append((y0, y, max(rows[y0:y])))
y += 1
for b in bands[:40]:
print("band y=%d..%d maxedge=%d" % b)
+81
View File
@@ -0,0 +1,81 @@
import re, os
txt = open(r"C:\Users\29452\AppData\Local\Temp\opencode\decoded\Fonts.dat.txt", encoding="latin1").read()
# tokenize blocks
def parse_block(s, i):
# s[i] should be at '{'
assert s[i] == '{'
i += 1
entries = [] # (name, value, childidx)
children = []
cur_name = None
while i < len(s):
# skip whitespace
while i < len(s) and s[i] in ' \t\r\n':
i += 1
if i >= len(s):
break
if s[i] == '}':
return entries, i+1
# read a name
m = re.match(r'[A-Za-z_#][A-Za-z0-9_#]*', s[i:])
if m:
name = m.group(0)
i += len(name)
while i < len(s) and s[i] in ' \t\r\n':
i += 1
if i < len(s) and s[i] == '=':
i += 1
while i < len(s) and s[i] in ' \t\r\n':
i += 1
if i < len(s) and s[i] == '"':
j = i+1
while j < len(s) and s[j] != '"':
j += 1
val = s[i+1:j]
i = j+1
else:
m2 = re.match(r'[^\s{}]+', s[i:])
val = m2.group(0) if m2 else ''
i += len(val)
entries.append((name, val))
elif i < len(s) and s[i] == '{':
sub, i = parse_block(s, i)
children.append((name, sub))
entries.append((name, sub))
else:
entries.append((name, ''))
else:
i += 1
return entries, i
# find top-level Font blocks
blocks = []
for m in re.finditer(r'\bFont\b\s*\{', txt):
start = txt.index('{', m.start())
ent, _ = parse_block(txt, start)
blocks.append(ent)
print("num Font blocks:", len(blocks))
for b in blocks:
d = dict((k, v) for k, v in b if k in ('Name','Language','Texture','#Texture','SpaceWidth','LineDistance','CharDistance'))
langs = [v for k, v in b if k == 'Language']
texs = [v for k, v in b if k in ('Texture', '#Texture')]
chars = [c for k, c in b if k == 'Char']
print("Font Name=%s langs=%s textures=%s #char=%d" % (d.get('Name'), langs, texs, len(chars)))
# print rect for 'e' in first block
def charmap(block):
cm = {}
for k, c in block:
if k == 'Char':
cd = dict((k2, v2) for k2, v2 in c)
cm[cd.get('Code')] = (int(cd.get('X1',0)), int(cd.get('Y1',0)), int(cd.get('X2',0)), int(cd.get('Y2',0)))
return cm
for bi, b in enumerate(blocks):
cm = charmap(b)
for ch in ['e','a','o','A',' ']:
if ch in cm:
print(f"block{bi} '{ch}' -> {cm[ch]}")
+57
View File
@@ -0,0 +1,57 @@
import argparse, csv, collections
def decide(flags, fourcc):
fs = set(flags.split(";")) if flags else set()
if "font_atlas" in fs or "non_texture" in fs:
return "skip", "font atlas uses fixed pixel rects in Fonts.dat / not a texture"
if "text_baked" in fs:
return "review", "text baked in image (must be redrawn, AI will garble text)"
if "ui_atlas" in fs:
return "review", "UI atlas, may have fixed pixel coordinates"
if "tiling_likely" in fs:
return "upscale_tiling", "tileable: use seamless/tiling-aware upscale"
return "upscale", ""
def main():
ap = argparse.ArgumentParser(description="Build an upscale plan csv from the preprocessing report.")
ap.add_argument("--report", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--scale", type=int, default=2)
args = ap.parse_args()
rows = list(csv.DictReader(open(args.report, newline="", encoding="utf-8")))
out = []
counts = collections.Counter()
for r in rows:
action, reason = decide(r.get("flags", ""), r.get("dds_fourcc", ""))
counts[action] += 1
try:
w = int(r["orig_w"]); h = int(r["orig_h"])
tw, th = w * args.scale, h * args.scale
except Exception:
tw = th = ""
out.append({
"name": r["name"],
"png": r["png"],
"orig_w": r["orig_w"], "orig_h": r["orig_h"],
"target_w": tw, "target_h": th,
"has_alpha": r.get("has_alpha", ""),
"dds_fourcc": r.get("dds_fourcc", ""),
"dds_mips": r.get("dds_mips", ""),
"group": r.get("group", ""),
"action": action,
"reason": reason,
})
with open(args.out, "w", newline="", encoding="utf-8") as f:
wtr = csv.DictWriter(f, fieldnames=list(out[0].keys()))
wtr.writeheader()
wtr.writerows(out)
print("plan written:", args.out)
for k, v in counts.most_common():
print(" %-16s %d" % (k, v))
if __name__ == "__main__":
main()
+79
View File
@@ -0,0 +1,79 @@
import struct, sys
from capstone import *
exe = r"F:\steam\steamapps\common\The I of the Dragon\TheIOfTheDragon.exe"
data = open(exe, "rb").read()
# --- parse PE ---
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
assert data[e_lfanew:e_lfanew+4] == b"PE\x00\x00", "not PE"
coff = e_lfanew + 4
machine, nsec, tstamp, symptr, nsym, optsize, chars = struct.unpack_from("<HHIIIHH", data, coff)
opt = coff + 20
magic = struct.unpack_from("<H", data, opt)[0]
print("machine=%04x sections=%d magic=%04x" % (machine, nsec, magic))
# image base
imagebase = struct.unpack_from("<I", data, opt+28)[0]
print("imagebase=%08x" % imagebase)
sec_off = opt + optsize
sections = []
for i in range(nsec):
o = sec_off + i*40
name = data[o:o+8].rstrip(b"\x00").decode("latin1")
vsize, vaddr, rawsize, rawptr = struct.unpack_from("<IIII", data, o+8)
sections.append((name, vaddr, vsize, rawptr, rawsize))
print("sec %-8s VA=%08x VS=%08x RAW=%08x RS=%08x" % (name, vaddr, vsize, rawptr, rawsize))
def off2va(off):
for name, va, vs, rp, rs in sections:
if rp <= off < rp+rs:
return va + (off-rp)
return None
def va2off(va):
for name, sva, vs, rp, rs in sections:
if sva <= va < sva+max(vs, rs):
return rp + (va-sva)
return None
# find strings of interest
targets = [b"Data\\Misc\\Fonts.dat", b"Data\\Misc\\Strings.dat", b"Data\\Misc\\UnitDescription.dat",
b"Data\\Misc\\AIDescription.dat", b"UI\\Overhead\\Shrift_gb.tga"]
str_vas = {}
for t in targets:
idx = data.find(t)
while idx != -1:
va = off2va(idx)
print(f"str {t!r} at off {idx:#x} VA {va:#x}")
str_vas[t] = va
idx = data.find(t, idx+1)
# text section
text = next(s for s in sections if s[0] == ".text")
tname, tva, tvs, trp, trs = text
code = data[trp:trp+trs]
md = Cs(CS_ARCH_X86, CS_MODE_32)
md.detail = True
# scan for instructions whose operand references target VAs (or a nearby data VA block)
want = set(v for v in str_vas.values() if v)
# also collect a window around the font string table maybe
def scan_for(refs, label):
hits = []
for ins in md.disasm(code, tva):
for op in ins.operands:
if op.type == CS_OP_IMM and op.imm in refs:
hits.append(ins)
print(f"--- refs to {label}: {len(hits)} ---")
for ins in hits[:20]:
print(f" {ins.address:08x}: {ins.mnemonic} {ins.op_str}")
scan_for(want, "target strings")
# Print data around the string table (Strings.dat / Fonts.dat region) to see structure
for t in [b"Data\\Misc\\Strings.dat"]:
if t in str_vas:
va = str_vas[t]
off = va2off(va)
print("=== dump around", t, "===")
print(data[off-64:off+320])
+93
View File
@@ -0,0 +1,93 @@
import os, re, struct
from PIL import Image, ImageDraw, ImageFont
GAME = r"F:\steam\steamapps\common\The I of the Dragon"
OUT = r"E:\DragonHD\patch"
TBL = os.path.join(OUT, "tables")
KEY = b"GBDFYTNE"
os.makedirs(TBL, exist_ok=True)
CODE = 0xC1FA # GB2312 "龙"
GLYPH = ""
GW = GH = 16
AX, AY = 122, 0 # free spot in Shrift_gb_Germany.TGA (256x128)
ATLAS = "Shrift_gb_Germany.TGA"
ADVANCE = 17
HEIGHT = 16
# ---- 1. render glyph ----
font = ImageFont.truetype(r"C:\Windows\Fonts\simhei.ttf", GH)
g = Image.new("RGBA", (GW, GH), (0, 0, 0, 0))
d = ImageDraw.Draw(g)
bb = d.textbbox((0, 0), GLYPH, font=font)
d.text(((GW-(bb[2]-bb[0]))/2 - bb[0], (GH-(bb[3]-bb[1]))/2 - bb[1]), GLYPH, fill=(255, 255, 255, 255), font=font)
gpx = g.load()
def tga_geom(raw):
w = struct.unpack_from("<H", raw, 12)[0]
h = struct.unpack_from("<H", raw, 14)[0]
return w, h, raw[17]
def add_glyph(raw):
w, h, desc = tga_geom(raw)
off = 18 + raw[0]
def row(y): return y if (desc & 0x20) else (h - 1 - y)
for dy in range(GH):
for dx in range(GW):
a = gpx[dx, dy][3]
if a == 0:
continue
i = off + (row(AY+dy) * w + (AX+dx)) * 4
raw[i] = 255; raw[i+1] = 255; raw[i+2] = 255; raw[i+3] = a
# inject into loose + res
loose = os.path.join(GAME, "Data", "Textures", "Ui", "Overhead", ATLAS)
for p in [loose, os.path.join(GAME, "Data", "Textures", "Ui", "Overhead", "Shrift_gb.tga")]:
if os.path.exists(p):
raw = bytearray(open(p, "rb").read())
add_glyph(raw)
open(p, "wb").write(raw)
print("injected loose", p)
res = os.path.join(GAME, "Data", "Textures.res")
data = bytearray(open(res, "rb").read())
cnt = struct.unpack_from("<I", data, 0)[0]; p = 4; ent = {}
for i in range(cnt):
ln = struct.unpack_from("<I", data, p)[0]; p += 4
nm = data[p:p+ln].decode("latin1"); p += ln
o, s = struct.unpack_from("<II", data, p); p += 8
ent[nm] = (o, s)
for nm in ["UI\\Overhead\\Shrift_gb_Germany.TGA", "UI\\Overhead\\Shrift_gb.tga"]:
if nm in ent:
o, s = ent[nm]
raw = bytearray(data[o:o+s])
add_glyph(raw)
data[o:o+s] = raw
print("injected res", nm)
open(res, "wb").write(data)
# ---- 2. cjk tables ----
glyph_tbl = bytearray(65536 * 16)
metric_tbl = bytearray(65536 * 8)
u1 = AX / 256.0; v1 = AY / 128.0; u2 = (AX + GW) / 256.0; v2 = (AY + GH) / 128.0
struct.pack_into("<4f", glyph_tbl, CODE * 16, u1, v1, u2, v2)
struct.pack_into("<2i", metric_tbl, CODE * 8, ADVANCE, HEIGHT)
open(os.path.join(TBL, "cjk_glyph.bin"), "wb").write(glyph_tbl)
open(os.path.join(TBL, "cjk_metric.bin"), "wb").write(metric_tbl)
print("tables written, code 0x%04X -> uv(%.4f,%.4f,%.4f,%.4f) advance=%d" % (CODE, u1, v1, u2, v2, ADVANCE))
# ---- 3. set test strings ----
strp = os.path.join(GAME, "Data", "Misc", "Strings.dat")
enc = open(strp, "rb").read()
test = bytes([CODE >> 8, CODE & 0xFF])
ids = ["MainMenu_StartNewGame", "MainMenu_LoadGame", "MainMenu_Options", "MainMenu_Credits",
"MainMenu_Exit", "MainMenu_ResumeGame", "MainMenu_SaveLoadGame", "MainMenu_Multi",
"WindowCaption"]
done = []
for sid in ids:
pat = re.compile((r'(Id\s*=\s*"%s"\s*[\r\n]+\s*English\s*=\s*")[^"]*(")' % re.escape(sid)).encode())
enc2 = pat.sub(lambda m: m.group(1) + test + m.group(2), enc, count=1)
if enc2 != enc:
done.append(sid)
enc = enc2
open(strp, "wb").write(enc)
print("test strings set:", done)
+76
View File
@@ -0,0 +1,76 @@
import os, re, struct
from PIL import Image, ImageDraw, ImageFont
GAME = r"F:\steam\steamapps\common\The I of the Dragon"
TBL = r"E:\DragonHD\patch\tables"
KEY = b"GBDFYTNE"
os.makedirs(TBL, exist_ok=True)
CODE = 0xC1FA
RED = (122, 0, 138, 16)
GREEN = (140, 0, 156, 16)
ATLAS = "Shrift_gb_Germany.TGA"
# 1. paint red + green solid rects into atlas
def paint(raw, x1, y1, x2, y2, b, g, r):
w = struct.unpack_from("<H", raw, 12)[0]
h = struct.unpack_from("<H", raw, 14)[0]
desc = raw[17]; off = 18 + raw[0]
row = (lambda y: y) if (desc & 0x20) else (lambda y: h - 1 - y)
for y in range(y1, y2):
for x in range(x1, x2):
i = off + (row(y) * w + x) * 4
raw[i] = b; raw[i+1] = g; raw[i+2] = r; raw[i+3] = 255
loose = os.path.join(GAME, "Data", "Textures", "Ui", "Overhead")
for p in [os.path.join(loose, "Shrift_gb.tga")]:
if os.path.exists(p):
d = bytearray(open(p, "rb").read()); paint(d, *RED, 0, 0, 255); paint(d, *GREEN, 0, 255, 0)
open(p, "wb").write(d); print("loose painted", p)
res = os.path.join(GAME, "Data", "Textures.res")
data = bytearray(open(res, "rb").read())
cnt = struct.unpack_from("<I", data, 0)[0]; p = 4; ent = {}
for i in range(cnt):
ln = struct.unpack_from("<I", data, p)[0]; p += 4
nm = data[p:p+ln].decode("latin1"); p += ln
o, s = struct.unpack_from("<II", data, p); p += 8; ent[nm] = (o, s)
for nm in ["UI\\Overhead\\Shrift_gb_Germany.TGA", "UI\\Overhead\\Shrift_gb.tga"]:
o, s = ent[nm]; b = bytearray(data[o:o+s]); paint(b, *RED, 0, 0, 255); paint(b, *GREEN, 0, 255, 0)
data[o:o+s] = b; print("res painted", nm)
open(res, "wb").write(data)
# 2. Fonts.dat: point UI English 'e' at RED
fp = os.path.join(GAME, "Data", "Misc", "Fonts.dat")
d = open(fp, "rb").read()
dec = bytes(d[2:][i] ^ KEY[i % len(KEY)] for i in range(len(d) - 2)).decode("latin1")
first = dec.index("Font"); second = dec.index("Font", first + 4)
block0 = dec[first:second]
pat = re.compile(r'(Code\s*=\s*"e"\s*X1\s*=\s*)(-?\d+)(\s*Y1\s*=\s*)(-?\d+)(\s*X2\s*=\s*)(-?\d+)(\s*Y2\s*=\s*)(-?\d+)')
block0b, n = pat.subn(lambda m: "%s%d%s%d%s%d%s%d" % (m.group(1), RED[0], m.group(3), RED[1], m.group(5), RED[2], m.group(7), RED[3]), block0, count=1)
dec2 = dec[:first] + block0b + dec[second:]
enc = b"==" + bytes(dec2.encode("latin1")[i] ^ KEY[i % len(KEY)] for i in range(len(dec2)))
open(fp, "wb").write(enc)
print("Fonts.dat 'e' -> red", n)
# 3. cjk tables: code -> GREEN
gt = bytearray(65536 * 16); mt = bytearray(65536 * 8)
u1 = GREEN[0] / 256.0; v1 = GREEN[1] / 128.0; u2 = GREEN[2] / 256.0; v2 = GREEN[3] / 128.0
struct.pack_into("<4f", gt, CODE * 16, u1, v1, u2, v2)
struct.pack_into("<2i", mt, CODE * 8, 17, 16)
open(os.path.join(TBL, "cjk_glyph.bin"), "wb").write(gt)
open(os.path.join(TBL, "cjk_metric.bin"), "wb").write(mt)
print("cjk 0x%04X -> green" % CODE)
# 4. strings: set menu ids to CODE
sp = os.path.join(GAME, "Data", "Misc", "Strings.dat")
encs = open(sp, "rb").read()
test = bytes([CODE >> 8, CODE & 0xFF])
pat2 = re.compile(rb'(Id\s*=\s*"([^"]*)"\s*[\r\n]+\s*English\s*=\s*")[^"]*(")')
def repl(m):
sid = m.group(2)
if sid.startswith((b"MainMenu", b"OptionsMenu", b"SaveLoadMenu", b"VideoOptions", b"AudioOptions")):
return m.group(1) + test + m.group(3)
return m.group(0)
encs2, n2 = pat2.subn(repl, encs)
open(sp, "wb").write(encs2)
print("menu strings set:", n2)
+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()
+72
View File
@@ -0,0 +1,72 @@
import argparse, os, struct, csv, sys
def align16(x):
return (x + 15) & ~15
def collect(args):
items = []
if args.csv and os.path.exists(args.csv):
with open(args.csv, newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
items.append((row["name"], row["file"]))
else:
for root, dirs, files in os.walk(args.dir):
for fn in files:
full = os.path.join(root, fn)
rel = os.path.relpath(full, args.dir)
name = rel.replace("/", "\\")
items.append((name, rel))
return items
def main():
ap = argparse.ArgumentParser(description="Rebuild a Dragon .res archive from a directory (16-byte aligned blobs).")
ap.add_argument("dir")
ap.add_argument("out")
ap.add_argument("--csv", default=None)
args = ap.parse_args()
items = collect(args)
blobs = []
for name, rel in items:
full = os.path.join(args.dir, rel)
if not os.path.isfile(full):
print("missing: %s" % full, file=sys.stderr)
sys.exit(2)
blobs.append((name, open(full, "rb").read()))
hdr = 4
for name, _ in blobs:
hdr += 4 + len(name) + 8
data_start = align16(hdr)
out = bytearray()
out += struct.pack("<I", len(blobs))
index = bytearray()
offset = data_start
payload = bytearray()
for i, (name, blob) in enumerate(blobs):
nb = name.encode("latin1")
index += struct.pack("<I", len(nb)) + nb + struct.pack("<II", offset, len(blob))
payload += blob
if i != len(blobs) - 1:
pad = align16(len(blob)) - len(blob)
payload += b"\x00" * pad
offset += len(blob) + pad
else:
offset += len(blob)
assert len(index) + 4 == hdr, (len(index) + 4, hdr)
out += index
out += b"\x00" * (data_start - len(out))
out += payload
with open(args.out, "wb") as f:
f.write(out)
print("repacked %d files -> %s (%d bytes)" % (len(blobs), args.out, len(out)))
if __name__ == "__main__":
main()
+68
View File
@@ -0,0 +1,68 @@
import argparse, os, struct, csv, hashlib, sys
def read_index(path):
data = open(path, "rb").read()
count = struct.unpack_from("<I", data, 0)[0]
p = 4
entries = []
for i in range(count):
ln = struct.unpack_from("<I", data, p)[0]
p += 4
name = data[p:p + ln].decode("latin1")
p += ln
off, size = struct.unpack_from("<II", data, p)
p += 8
entries.append((name, off, size))
return data, entries, p
def safe_rel(name):
rel = name.replace("\\", os.sep).replace("/", os.sep)
parts = [x for x in rel.split(os.sep) if x not in ("", ".", "..")]
return os.path.join(*parts) if parts else "_unnamed"
def main():
ap = argparse.ArgumentParser(description="Extract the Dragon .res archive (index + raw blobs).")
ap.add_argument("res")
ap.add_argument("out")
ap.add_argument("--csv", default=None)
args = ap.parse_args()
data, entries, header_end = read_index(args.res)
os.makedirs(args.out, exist_ok=True)
rows = []
used = set()
for name, off, size in entries:
rel = safe_rel(name)
dst = os.path.join(args.out, rel)
base, ext = os.path.splitext(dst)
n = 1
while dst.lower() in used:
dst = "%s__%d%s" % (base, n, ext)
n += 1
used.add(dst.lower())
os.makedirs(os.path.dirname(dst), exist_ok=True)
blob = data[off:off + size]
with open(dst, "wb") as f:
f.write(blob)
rows.append({
"name": name,
"offset": off,
"size": size,
"file": os.path.relpath(dst, args.out),
"sha1": hashlib.sha1(blob).hexdigest(),
})
print("[%4d/%d] %s (%d bytes)" % (len(rows), len(entries), name, size))
if args.csv:
with open(args.csv, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=["name", "offset", "size", "file", "sha1"])
w.writeheader()
w.writerows(rows)
print("done: %d files -> %s" % (len(rows), args.out))
if __name__ == "__main__":
main()
+45
View File
@@ -0,0 +1,45 @@
import struct
from capstone import *
exe = r"F:\steam\steamapps\common\The I of the Dragon\TheIOfTheDragon.exe"
data = open(exe, "rb").read()
e = struct.unpack_from("<I", data, 0x3C)[0]
coff = e + 4
nsec = struct.unpack_from("<H", data, coff + 2)[0]
optsize = struct.unpack_from("<H", data, coff + 16)[0]
opt = coff + 20
imagebase = struct.unpack_from("<I", data, opt + 28)[0]
so = opt + optsize
secs = []
for i in range(nsec):
o = so + i * 40
nm = data[o:o+8].rstrip(b"\x00").decode("latin1")
vs, va, rs, rp = struct.unpack_from("<IIII", data, o + 8)
secs.append((nm, va, vs, rp, rs))
text = next(s for s in secs if s[0] == ".text")
name, tva, tvs, trp, trs = text
blob = data[trp:trp+trs]
md = Cs(CS_ARCH_X86, CS_MODE_32)
for tag, dispbytes in [("0x101c metrics", b"\x1c\x10\x00\x00"), ("0x181c flag", b"\x1c\x18\x00\x00")]:
print("================", tag)
i = 0
found = []
while True:
j = blob.find(dispbytes, i)
if j == -1:
break
found.append(j)
i = j + 1
print("byte occurrences:", len(found))
for j in found:
# disassemble a window ending at this disp; the instruction starts a few bytes earlier
start = max(0, j - 6)
addr = imagebase + tva + start
ctx = []
for ins in md.disasm(blob[start:j+8], addr):
ctx.append("%08x: %s %s" % (ins.address, ins.mnemonic, ins.op_str))
print("--- at file 0x%x VA 0x%x" % (j, imagebase + tva + j))
for c in ctx[-3:]:
print(" ", c)
+38
View File
@@ -0,0 +1,38 @@
import struct
from capstone import *
from capstone.x86 import X86_OP_MEM, X86_REG_RIP
exe = r"F:\steam\steamapps\common\The I of the Dragon\TheIOfTheDragon.exe"
data = open(exe, "rb").read()
e = struct.unpack_from("<I", data, 0x3C)[0]
coff = e + 4
nsec = struct.unpack_from("<H", data, coff + 2)[0]
optsize = struct.unpack_from("<H", data, coff + 16)[0]
opt = coff + 20
imagebase = struct.unpack_from("<I", data, opt + 28)[0]
so = opt + optsize
secs = []
for i in range(nsec):
o = so + i * 40
nm = data[o:o+8].rstrip(b"\x00").decode("latin1")
vs, va, rs, rp = struct.unpack_from("<IIII", data, o + 8)
secs.append((nm, va, vs, rp, rs))
text = next(s for s in secs if s[0] == ".text")
name, tva, tvs, trp, trs = text
code = data[trp:trp+trs]
md = Cs(CS_ARCH_X86, CS_MODE_32)
md.detail = True
targets = {0x1c: "glyph[?]", 0x101c: "metrics", 0x181c: "usedflag"}
hits = {k: [] for k in targets}
for ins in md.disasm(code, imagebase + tva):
if ins.mnemonic == "ret" or ins.mnemonic.startswith("j"):
continue
for op in ins.operands:
if op.type == X86_OP_MEM and op.mem.disp in targets:
hits[op.mem.disp].append((ins.address, ins.mnemonic, ins.op_str))
for k, v in hits.items():
print("=== disp 0x%x (%s): %d refs ===" % (k, targets[k], len(v)))
for a, m, o in v[:40]:
print(" %08x: %s %s" % (a, m, o))
+73
View File
@@ -0,0 +1,73 @@
from PIL import Image
import glob, statistics
files = sorted(glob.glob(r"C:\Users\29452\AppData\Local\Temp\opencode\seq*.png"))
imgs = [Image.open(f).convert("L") for f in files]
W, H = imgs[0].size
n = len(imgs)
pxs = [im.load() for im in imgs]
# temporal std per pixel (sample every pixel but coarse for speed? do full)
# spatial gradient on mean image
# Build mean and std
import array
step = 1
cand = Image.new("L", (W, H), 0)
cp = cand.load()
for y in range(0, H, 2):
for x in range(0, W, 2):
vals = [px[x, y] for px in pxs]
m = sum(vals) / n
var = sum((v - m) ** 2 for v in vals) / n
std = var ** 0.5
# spatial contrast from mean image
xm = x - 2 if x - 2 >= 0 else x
xp = x + 2 if x + 2 < W else x
gx = abs(pxs[0][xp, y] - pxs[0][xm, y]) # placeholder
# use mean image gradient
mean_px = [[sum(px[xx, yy] for px in pxs) / n for xx in (x,)] for yy in (y,)]
cp[x, y] = 255 - min(255, int(std))
import os
# Better: compute static mask = std < 6, then high contrast of mean
mean = Image.new("L", (W, H), 0)
mp = mean.load()
for y in range(H):
for x in range(W):
mp[x, y] = int(sum(px[x, y] for px in pxs) / n)
# gradient of mean
ed = Image.new("L", (W, H), 0)
ep = ed.load()
for y in range(1, H - 1):
for x in range(1, W - 1):
g = abs(mp[x + 1, y] - mp[x - 1, y]) + abs(mp[x, y + 1] - mp[x, y - 1])
ep[x, y] = min(255, g)
# static mask
st = Image.new("L", (W, H), 0)
sp = st.load()
for y in range(H):
for x in range(W):
vals = [px[x, y] for px in pxs]
m = sum(vals) / n
var = sum((v - m) ** 2 for v in vals) / n
sp[x, y] = 255 if var ** 0.5 < 4 else 0
# candidate = static AND edge
cand = Image.new("L", (W, H), 0)
cp = cand.load()
for y in range(H):
for x in range(W):
cp[x, y] = ep[x, y] if sp[x, y] else 0
cand.save(r"C:\Users\29452\AppData\Local\Temp\opencode\staticcand.png")
# report bounding regions: coarse grid of activity
gw, gh = 40, 24
cellw, cellh = W // gw, H // gh
print("grid activity (static-edge):")
for gy in range(gh):
line = ""
for gx in range(gw):
s = 0
for y in range(gy * cellh, min(H, (gy + 1) * cellh), 3):
for x in range(gx * cellw, min(W, (gx + 1) * cellw), 3):
s += cp[x, y]
line += " .:-=+*#%@"[min(9, s // 300)]
print(line)
+22
View File
@@ -0,0 +1,22 @@
import re, shutil, sys
GAME = r"F:\steam\steamapps\common\The I of the Dragon"
BK = r"C:\Users\29452\AppData\Local\Temp\opencode\backup\Strings.dat"
p = GAME + r"\Data\Misc\Strings.dat"
shutil.copyfile(BK, p)
enc = open(p, "rb").read()
test = b"\xc1\xfa"
pat = re.compile(rb'(Id\s*=\s*"([^"]*)"\s*[\r\n]+\s*English\s*=\s*")[^"]*(")')
def repl(m):
sid = m.group(2)
if b"Filename" in sid or b"Path" in sid:
return m.group(0)
return m.group(1) + test + m.group(3)
enc2, n = pat.subn(repl, enc)
open(p, "wb").write(enc2)
print("strings replaced:", n)
+67
View File
@@ -0,0 +1,67 @@
import struct, os, collections
res = r"F:\steam\steamapps\common\The I of the Dragon\Data\Textures.res"
data = open(res, "rb").read()
count = struct.unpack_from("<I", data, 0)[0]
p = 4
entries = []
for i in range(count):
ln = struct.unpack_from("<I", data, p)[0]; p += 4
nm = data[p:p+ln].decode("latin1"); p += ln
o, s = struct.unpack_from("<II", data, p); p += 8
entries.append((nm, o, s))
print("total entries:", len(entries))
ext = collections.Counter(os.path.splitext(n)[1].lower() for n, _, _ in entries)
print("extensions:", dict(ext))
dim_hist = collections.Counter()
fmt_hist = collections.Counter()
alpha_hist = collections.Counter()
sizes = []
problems = []
total_pixels = 0
def tga_info(b):
idlen = b[0]; cmap = b[1]; typ = b[2]
w = struct.unpack_from("<H", b, 12)[0]
h = struct.unpack_from("<H", b, 14)[0]
bpp = b[16]
return typ, w, h, bpp
def dds_info(b):
h = struct.unpack_from("<I", b, 12)[0]
w = struct.unpack_from("<I", b, 16)[0]
four = b[84:88]
return w, h, four
for nm, o, s in entries:
e = os.path.splitext(nm)[1].lower()
b = data[o:o+s]
try:
if e == ".tga":
typ, w, h, bpp = tga_info(b)
fmt_hist[f"TGA{typ}/{bpp}bpp"] += 1
dim_hist[f"{w}x{h}"] += 1
total_pixels += w*h
if bpp == 32:
alpha_hist["TGA32(has alpha channel)"] += 1
elif e == ".dds":
w, h, four = dds_info(b)
fmt_hist["DDS:"+four.decode("latin1").strip()] += 1
dim_hist[f"{w}x{h}"] += 1
total_pixels += w*h
else:
fmt_hist["other:"+e] += 1
except Exception as ex:
problems.append((nm, s, str(ex)))
print("\nformats:")
for k, v in fmt_hist.most_common():
print(" %-24s %d" % (k, v))
print("\ntop dimensions:")
for k, v in dim_hist.most_common(20):
print(" %-12s %d" % (k, v))
print("\nalpha:", dict(alpha_hist))
print("\ntotal pixels: %.1f Mpix (%.0f MB as RGBA8)" % (total_pixels/1e6, total_pixels*4/1e6))
print("problems:", problems[:5], "...total", len(problems))
+17
View File
@@ -0,0 +1,17 @@
from PIL import Image
import sys
p = sys.argv[1]
cols = int(sys.argv[2]) if len(sys.argv) > 2 else 120
img = Image.open(p).convert("L")
W, H = img.size
rows = max(1, int(cols * H / W / 2.1))
img2 = img.resize((cols, rows))
px = img2.load()
ramp = " .:-=+*#%@"
for y in range(rows):
line = ""
for x in range(cols):
v = px[x, y]
line += ramp[min(9, (255 - v) * 10 // 256)]
print(line)
print("size", W, H, "->", cols, rows)
+44
View File
@@ -0,0 +1,44 @@
import struct, sys
exe = r"F:\steam\steamapps\common\The I of the Dragon\TheIOfTheDragon.exe"
data = open(exe, "rb").read()
e = struct.unpack_from("<I", data, 0x3C)[0]
coff = e + 4
nsec = struct.unpack_from("<H", data, coff + 2)[0]
optsize = struct.unpack_from("<H", data, coff + 16)[0]
opt = coff + 20
imagebase = struct.unpack_from("<I", data, opt + 28)[0]
so = opt + optsize
secs = []
for i in range(nsec):
o = so + i * 40
nm = data[o:o+8].rstrip(b"\x00").decode("latin1")
vs, va, rs, rp = struct.unpack_from("<IIII", data, o + 8)
secs.append((nm, va, vs, rp, rs))
def va2off(va):
rva = va - imagebase
for nm, sva, vs, rp, rs in secs:
if sva <= rva < sva + max(vs, rs):
return rp + (rva - sva)
return None
def find_xrefs(target):
res = []
for nm, sva, vs, rp, rs in secs:
if nm != ".text":
continue
blob = data[rp:rp+rs]
for i in range(len(blob)-5):
if blob[i] == 0xE8 or blob[i] == 0xE9:
rel = struct.unpack_from("<i", blob, i+1)[0]
addr = imagebase + sva + i
if addr + 5 + rel == target:
res.append((addr, "call" if blob[i] == 0xE8 else "jmp"))
return res
for t in [int(x, 16) for x in sys.argv[1:]]:
r = find_xrefs(t)
print("=== xrefs to %08x : %d ===" % (t, len(r)))
for a, k in r:
print(" %08x %s" % (a, k))