# make_pack.py <out folder> [game folder] [--tools <folder with mafiam_studio.exe>]: the DLC pack that puts the carcyclopedia's hall into Diamond Motors
# (the showroom by the Midtown bridge), for a server's dlcpacks/ folder:
#   replace/pc/sds/shops/show.sds          the hall moved (MafiaM Studio --relocate), its seller and guard taken out
#   replace/EDIT/tables/StreamMapa.bin     show.sds loaded in free ride (its loader's lines 1329..1330 -> 1092..1104)
#   sds/patches/sds/city/italy.sds.patch   the town's Diamond Motors opened: its frosted glass (SHOW) hidden, the
#                                          wall across its storefront taken out of the collision
# Needs MafiaM Studio 0.2 or newer (mafiam_studio.exe and mafiam_sds.exe, from mafiam.com/tools): next to this
# script, in the --tools folder, or MafiaM's own build (build/x64/bin/Release). Python 3, nothing else.
#
# Found making it (see also tools/studio/src/relocate.h):
# - the hall's outside (its "venek" mesh) is Diamond Motors itself: turned 110 degrees it lands on the town's
#   building to 2 mm - that's where the hall goes
# - a replaced .sds must not be bigger than the game's own, or its graphics don't load
# - free ride doesn't use Joe's Adventures' patches (its mounts have TriggerId="GameLine_6"): a pack's patch
#   applies to the game's own italy.sds. A patch takes resources out by number and adds its records at the end -
#   kept in the type order (the frames before what refers to them), or the game hangs loading
import json, math, os, shutil, struct, subprocess, sys, tempfile, zlib

args = sys.argv[1:]
tools = None
if "--tools" in args:
    k = args.index("--tools")
    tools = args[k + 1]
    del args[k:k + 2]
if not args:
    sys.exit(__doc__ or "make_pack.py <out folder> [game folder] [--tools <folder>]")
out = args[0]
game = args[1] if len(args) > 1 else r"C:\Program Files (x86)\Steam\steamapps\common\Mafia II Definitive Edition"
here = os.path.dirname(os.path.abspath(__file__))
places = [tools] if tools else []
places += [here, os.path.join(os.path.dirname(os.path.dirname(here)), "build", "x64", "bin", "Release")]
bin_ = next((d for d in places if d and os.path.isfile(os.path.join(d, "mafiam_studio.exe"))), None)
if not bin_:
    sys.exit("mafiam_studio.exe not found: put this script next to it or give --tools <its folder>")
if not os.path.isfile(os.path.join(game, "pc", "sds", "shops", "show.sds")):
    sys.exit("the game isn't in " + game + ": give its folder (the one with pc and EDIT)")
studio, sds = os.path.join(bin_, "mafiam_studio.exe"), os.path.join(bin_, "mafiam_sds.exe")
tmp = tempfile.mkdtemp(prefix="showroom_")

def run(*args):
    subprocess.run(args, check=True)

def put(rel, data=None, src=None):
    path = os.path.join(out, *rel.split("/"))
    os.makedirs(os.path.dirname(path), exist_ok=True)
    if src:
        shutil.copyfile(src, path)
    else:
        open(path, "wb").write(data)
    print(rel, os.path.getsize(path))

# 1. the hall: the point of its turntable onto the round corner of Diamond Motors
show = os.path.join(game, "pc", "sds", "shops", "show.sds")
moved = os.path.join(tmp, "show.sds")
run(studio, "--relocate", show, moved, "-292.9", "823.5", "0", "-188.886", "842.832", "-31.085", "110",
    "hide=SR_prodavac,SR_ochranka")
assert os.path.getsize(moved) <= os.path.getsize(show), "the hall came out bigger than the game's own file"
put("replace/pc/sds/shops/show.sds", src=moved)

# 2. the stream map: show.sds's loader on the free ride lines
table = bytearray(open(os.path.join(game, "EDIT", "tables", "StreamMapa.bin"), "rb").read())
loader = struct.pack("<6i", 1329, 1330, 4, 0, 295, 1)
at = table.find(loader)
assert at >= 0 and table.find(loader, at + 1) < 0, "show.sds's loader not found once"
struct.pack_into("<2i", table, at, 1092, 1104)
put("replace/EDIT/tables/StreamMapa.bin", bytes(table))

# 3. italy.sds: SHOW hidden (frames), the storefront wall out (collision); resources 480..495 again, in order
italy = os.path.join(game, "pc", "sds", "city", "italy.sds")
hidden = os.path.join(tmp, "italy_hidden.sds")
run(studio, "--relocate", italy, hidden, "0", "0", "0", "0", "0", "0", "0", "hide=SHOW")
base, hid = os.path.join(tmp, "italy"), os.path.join(tmp, "italy_hidden")
run(sds, "unpack", italy, base)
run(sds, "unpack", hidden, hid)
man = json.load(open(os.path.join(base, "manifest.json")))
hman = json.load(open(os.path.join(hid, "manifest.json")))
tid = {t["name"]: t["id"] for t in man["types"]}
first, last = 480, 495
assert man["resources"][first]["type"] == "FrameResource" and man["resources"][last]["type"] == "Collisions"

def clear_storefront(d):
    # Diamond Motors' collision (placed at -212.81, 834.33): its triangles of the walls across the storefront
    # made empty. A placement's mesh is put by its rotation's transpose.
    d = bytearray(d)
    places = struct.unpack_from("<I", d, 8)[0]
    pl = [(struct.unpack_from("<3f", d, 12 + i * 37), struct.unpack_from("<3f", d, 24 + i * 37),
           struct.unpack_from("<Q", d, 36 + i * 37)[0]) for i in range(places)]
    p = 12 + places * 37
    count = struct.unpack_from("<I", d, p)[0]
    p += 4
    meshes = {}
    for _ in range(count):
        h, size = struct.unpack_from("<QI", d, p)
        p += 12
        meshes[h] = p
        p += size
        p += 4 + 16 * struct.unpack_from("<I", d, p)[0]
    lo, hi = (-202.6, 837.6, -21.6), (-196.5, 840.4, -17.5)
    cleared = 0
    for pos, rot, h in pl:
        if abs(pos[0] + 212.81) > 0.05 or abs(pos[1] - 834.33) > 0.05:
            continue
        at = meshes[h]
        flags, = struct.unpack_from("<I", d, at + 12)
        nv, nt = struct.unpack_from("<II", d, at + 28)
        size = 1 if flags & 8 else 2 if flags & 16 else 4
        fmt = {1: "<3B", 2: "<3H", 4: "<3I"}[size]
        c, s = math.cos(rot[2]), math.sin(rot[2])
        world = []
        for k in range(nv):
            x, y, z = struct.unpack_from("<3f", d, at + 36 + 12 * k)
            world.append((x * c + y * s + pos[0], -x * s + y * c + pos[1], z + pos[2]))
        tris = at + 36 + nv * 12
        for t in range(nt):
            tri = struct.unpack_from(fmt, d, tris + t * 3 * size)
            pts = [world[v] for v in tri]
            inside = all(lo[i] <= q[i] <= hi[i] for q in pts for i in range(3))
            if inside and max(q[2] for q in pts) - min(q[2] for q in pts) > 2:
                struct.pack_into(fmt, d, tris + t * 3 * size, tri[0], tri[0], tri[0])
                cleared += 1
    assert cleared, "the storefront wasn't found"
    print("storefront triangles cleared:", cleared)
    return bytes(d)

def fnv1(data, h=0x811C9DC5):
    for x in data:
        h = ((h * 0x01000193) & 0xFFFFFFFF) ^ x
    return h

records = bytearray()
indices = list(range(first, last + 1))
for i in indices:
    e = man["resources"][i]
    data = open(os.path.join(base, "resources", e["file"]), "rb").read()
    if i == first:
        data = open(os.path.join(hid, "resources", hman["resources"][first]["file"]), "rb").read()
    if i == last:
        data = clear_storefront(data)
    head = struct.pack("<IIHIIII", tid[e["type"]], 30 + len(data), e["version"], e["slot_ram"], e["slot_vram"],
                       e["other_ram"], e["other_vram"])
    records += head + struct.pack("<I", fnv1(head)) + data
patch = bytearray(bytes.fromhex("0f0f010d020000000d01f0f000000000"))
patch += struct.pack("<I", len(indices)) + struct.pack("<%dI" % len(indices), *indices)
patch += struct.pack("<II", 0, len(indices)) + b"UEzl" + struct.pack("<IB", 0x4000, 4)
for at in range(0, len(records), 0x4000):
    block = bytes(records[at:at + 0x4000])
    z = zlib.compress(block, 9)
    if 32 + len(z) >= len(block):
        patch += struct.pack("<IB", len(block), 0) + block
        continue
    patch += struct.pack("<IB", 32 + len(z), 1)
    patch += struct.pack("<IIHHHHIIII", len(block), 32, 0x4000, 1, 1, 0x080F, len(z), 0, 0, 0) + z
patch += struct.pack("<IB", 0, 0)
put("sds/patches/sds/city/italy.sds.patch", bytes(patch))
shutil.rmtree(tmp, ignore_errors=True)
