#!/usr/bin/env python3
"""Dump CardsDLL's NULL-terminated {const char*, int} vocabulary tables from the
ON-DISK PE. READ-ONLY, static.

The transfer-market analysis records tradeState as decoding through a table walk at
0x180229e40 and lists sibling vocabularies (type/zone/lev/pos) as tables of the same
shape. This prints the exact token spellings and their integer codes, so the accepted
strings come from the client rather than from inference.
"""
import struct

DLL = "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll"
TABLES = [
    (0x180229E40, "tradeState (table walk)"),
    (0x180229C30, "type"),
    (0x1802296E0, "zone"),
    (0x180229A60, "lev"),
    (0x1802295C0, "pos"),
    (0x180229AB0, "cat"),
    (0x180229880, "form"),
]
MAX_ROWS = 64

pe = open(DLL, "rb").read()
e_lfanew = struct.unpack_from("<I", pe, 0x3C)[0]
coff = e_lfanew + 4
num_sections = struct.unpack_from("<H", pe, coff + 2)[0]
opt_size = struct.unpack_from("<H", pe, coff + 16)[0]
opt = coff + 20
image_base = struct.unpack_from("<Q", pe, opt + 24)[0]
sec_off = opt + opt_size
sections = []
for i in range(num_sections):
    b = sec_off + i * 40
    vsize, vaddr, rawsize, rawptr = struct.unpack_from("<IIII", pe, b + 8)
    sections.append((vaddr, vsize, rawptr, rawsize))


def va2off(va):
    rva = va - image_base
    for vaddr, vsize, rawptr, rawsize in sections:
        if vaddr <= rva < vaddr + max(vsize, rawsize):
            off = rva - vaddr + rawptr
            if 0 <= off < len(pe):
                return off
    return None


def cstr(va, limit=64):
    off = va2off(va)
    if off is None:
        return None
    end = pe.find(b"\0", off, off + limit)
    if end < 0:
        return None
    try:
        s = pe[off:end].decode("ascii")
    except UnicodeDecodeError:
        return None
    return s if s.isprintable() else None


for table_va, name in TABLES:
    base = va2off(table_va)
    print("\n=== %s  VA %#x -> off %s ===" % (name, table_va, hex(base) if base else None))
    if base is None:
        print("  (VA did not resolve)")
        continue
    for i in range(MAX_ROWS):
        ptr, code = struct.unpack_from("<Qi", pe, base + i * 16)
        if ptr == 0:
            print("  -- NULL terminator after %d rows --" % i)
            break
        s = cstr(ptr)
        if s is None:
            print("  row %d: ptr %#x does not resolve to a string; stopping" % (i, ptr))
            break
        print("  %-28s = %d" % (repr(s), code))
