"""Model of the game's codec-setup bump-allocator usage (dwDecodeAllocSize). Reversed from the dumped BatmanAC.exe (see docs/reverse-engineering/decoder.md): WwiseVorbisParseSetupHeader (runtime 0x010B4D10) and its callees are a Tremor-lowmem derivative; every pool allocation is rounded up to 4 bytes. """ from .bits import BitReader, ilog, book_maptype1_quantvals from .codebooks import CodebookLibrary def a4(n): return (n + 3) & ~3 def _node_bytes(used, leafwidth): if used < 2: return 4 if leafwidth == 3: leafwidth = 4 if ilog(3 * used - 6) + 1 <= leafwidth * 4: return leafwidth // 2 if leafwidth // 2 else 1 return leafwidth def _leaf_words(nodeb, leafwidth): return 2 if leafwidth > nodeb else 1 def _decode_table(used, nodeb, leafw): if nodeb == 4: return used * 8 + 4 return a4((used * (leafw + 1) - 2) * nodeb) def codebook_alloc(blob: bytes) -> int: """Pool bytes used when unpacking one compact Wwise codebook.""" r = BitReader(blob) dims = r.get(4) entries = r.get(14) if r.get(1): # ordered r.get(5) cur = 0 while cur < entries: cur += r.get(ilog(entries - cur)) used = entries else: cwll = r.get(3) sparse = r.get(1) used = 0 for _ in range(entries): if not sparse or r.get(1): r.get(cwll) used += 1 maptype = r.get(1) if maptype == 0: lw = ilog(entries) // 8 + 1 nodeb = _node_bytes(used, lw) return _decode_table(used, nodeb, _leaf_words(nodeb, lw)) r.get(32); r.get(32) qbits = r.get(4) + 1 quantvals = book_maptype1_quantvals(entries, dims) total1 = (qbits * dims + 8) // 8 total2 = (ilog(quantvals - 1) * dims + 8) // 8 + (qbits + 7) // 8 if total1 <= 4 and total1 <= total2: nodeb = _node_bytes(used, total1) return _decode_table(used, nodeb, _leaf_words(nodeb, total1)) qv = a4(quantvals) if qbits <= 8 else a4(quantvals * 2) lw = (ilog(quantvals - 1) * dims + 8) // 8 nodeb = _node_bytes(used, lw) return qv + _decode_table(used, nodeb, _leaf_words(nodeb, lw)) def setup_alloc(ww_setup: bytes, channels: int, lib: CodebookLibrary) -> dict: """Walk a stripped Wwise setup and sum pool allocations, by section.""" r = BitReader(ww_setup) out = {} nb = r.get(8) + 1 total = nb * 0x3C cb = 0 for _ in range(nb): cb += codebook_alloc(lib.books[r.get(10)]) out["codebooks"] = total + cb nf = r.get(6) + 1 fl = nf * 0x24 for _ in range(nf): parts = r.get(5) classes = [r.get(4) for _ in range(parts)] maxc = max(classes) if classes else -1 dims = [] for _ in range(maxc + 1): dims.append(r.get(3) + 1) sub = r.get(2) if sub: r.get(8) for _ in range(1 << sub): r.get(8) r.get(2) rb = r.get(4) posts = sum(dims[c] for c in classes) r.pos += posts * rb fl += a4(parts) + a4(11 * (maxc + 1)) + a4(2 * (posts + 2)) + a4(posts + 2) + 2 * a4(posts) out["floors"] = fl floors_count = nf nr = r.get(6) + 1 rs = nr * 0x1C for _ in range(nr): r.get(2); r.get(24); r.get(24); r.get(24) ncls = r.get(6) + 1 r.get(8) casc = [] for _ in range(ncls): lo = r.get(3) hi = r.get(5) if r.get(1) else 0 casc.append(hi * 8 + lo) for c in casc: r.pos += 8 * bin(c & 0xFF).count("1") rs += a4(ncls) + 8 * ncls out["residues"] = rs nm = r.get(6) + 1 ms = nm * 0x14 for _ in range(nm): submaps = r.get(4) + 1 if r.get(1) else 1 if r.get(1): steps = r.get(8) + 1 b = ilog(channels - 1) r.pos += steps * 2 * b ms += a4(2 * steps) r.get(2) if submaps > 1: r.pos += 4 * channels ms += a4(channels) r.pos += 24 * submaps ms += a4(2 * submaps) out["mappings"] = ms nmodes = r.get(6) + 1 out["modes"] = a4(2 * nmodes) out["total"] = sum(out.values()) out["floors_count"] = floors_count return out def fnv1_32(b: bytes) -> int: """uHashCodebook: FNV-1 32-bit over the stripped setup packet (verified on all 24 game setups).""" h = 0x811C9DC5 for x in b: h = (h * 0x01000193) & 0xFFFFFFFF h ^= x return h def decode_alloc(ww_setup: bytes, channels: int, lib: CodebookLibrary): """(dwDecodeAllocSize, dwDecodeX64AllocSize) as the 2011.1 encoder writes them. 32-bit: 0x30 (setup info) + parser allocations + 4*nbooks + 8 -> exact for all 24 game setups. x64: fitted (alloc + 8*nbooks + 40 + 52*nfloors); not read by the 32-bit game. """ a = setup_alloc(ww_setup, channels, lib) nbooks = ww_setup[0] + 1 alloc = 0x30 + a["total"] + 4 * nbooks + 8 nfloors = a["floors_count"] return alloc, alloc + 8 * nbooks + 40 + 52 * nfloors