Initial commit: RE tooling and docs for The I of the Dragon localization/HD
This commit is contained in:
+27
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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`).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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]}")
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user