Files
benilla-octo/Optional/sky-and-grading/tools/dbc_build.py
T
pkuzicandGPT-5.6 Sol (Codex) 67388625ec MONKEY (optional): add local sky and grading patch builder
Build Blizzard-derived sky assets only from the user's own CASC install, merge lighting DBCs from mounted winners, and pack a preserved patch-Z candidate. Include bilingual install and recovery guidance plus project-owned grading data.

Verified all 46 default stage payloads byte-identical to the reviewed stage; moderated WoW Forever LightIntBand byte-identical; band and mounted-chain checks pass on a hard-linked client copy. No Cargo command run.

Co-Authored-By: GPT-5.6 Sol (Codex)
2026-09-26 15:19:25 +01:00

201 lines
7.5 KiB
Python

"""Winner-safe lighting DBC merge for the optional sky patch."""
from __future__ import annotations
import struct
from typing import Dict, List, Set, Tuple
REQUIRED = (
"Light.dbc",
"LightParams.dbc",
"LightIntBand.dbc",
"LightFloatBand.dbc",
"LightSkybox.dbc",
)
def parse_dbc(data: bytes, label: str) -> Tuple[List[List[int]], bytes, int]:
if len(data) < 20:
raise ValueError("truncated DBC: %s" % label)
magic, count, fields, record_size, string_size = struct.unpack_from("<4s4I", data)
if magic != b"WDBC" or record_size != fields * 4:
raise ValueError("invalid WDBC schema: %s" % label)
expected = 20 + count * record_size + string_size
if len(data) != expected:
raise ValueError("invalid WDBC length: %s" % label)
records = [
list(struct.unpack_from("<%dI" % fields, data, 20 + i * record_size))
for i in range(count)
]
return records, data[20 + count * record_size :], fields
def pack_dbc(records: List[List[int]], strings: bytes, fields: int) -> bytes:
if any(len(record) != fields for record in records):
raise ValueError("inconsistent DBC record width")
header = struct.pack("<4s4I", b"WDBC", len(records), fields, fields * 4, len(strings))
body = b"".join(struct.pack("<%dI" % fields, *record) for record in records)
return header + body + strings
def indexed(records: List[List[int]], label: str) -> Dict[int, List[int]]:
result = {record[0]: record for record in records}
if len(result) != len(records):
raise ValueError("duplicate row ID in %s" % label)
return result
def band_row_id(param: int, band: int, per: int) -> int:
return (param - 1) * per + band + 1
def append_string(block: bytearray, value: str) -> int:
encoded = value.encode("utf-8") + b"\0"
start = 0
while True:
offset = block.find(encoded, start)
if offset < 0:
offset = len(block)
block.extend(encoded)
return offset
if offset == 0 or block[offset - 1] == 0:
return offset
start = offset + 1
def build_merged(winners: Dict[str, bytes]) -> Tuple[Dict[str, bytes], Dict[str, object]]:
missing = [name for name in REQUIRED if name not in winners]
if missing:
raise ValueError("missing winning source DBCs: %s" % ", ".join(missing))
light, light_strings, light_fields = parse_dbc(winners["Light.dbc"], "Light")
params, params_strings, params_fields = parse_dbc(
winners["LightParams.dbc"], "LightParams"
)
ints, int_strings, int_fields = parse_dbc(
winners["LightIntBand.dbc"], "LightIntBand"
)
floats, float_strings, float_fields = parse_dbc(
winners["LightFloatBand.dbc"], "LightFloatBand"
)
skies, sky_strings, sky_fields = parse_dbc(
winners["LightSkybox.dbc"], "LightSkybox"
)
if (light_fields, params_fields, int_fields, float_fields, sky_fields) != (
12, 9, 34, 34, 2
):
raise ValueError("unexpected source lighting schema; use an unmodified 1.12 DBC chain")
if any(block != b"\0" for block in (
light_strings, params_strings, int_strings, float_strings
)):
raise ValueError("numeric lighting DBC has a non-empty string block")
light_by_id = indexed(light, "Light")
params_by_id = indexed(params, "LightParams")
int_by_id = indexed(ints, "LightIntBand")
float_by_id = indexed(floats, "LightFloatBand")
targets = {20: 1139, 539: 1139, 6: 1140, 65: 1140, 67: 1140}
expected = {20: 23, 539: 23, 6: 21, 65: 21, 67: 21}
if light_by_id.get(14, [None] * 8)[7] != 16:
raise ValueError("Light row 14 is not on expected Swamp of Sorrows param 16")
for row_id, new_param in targets.items():
if row_id not in light_by_id or light_by_id[row_id][7] != expected[row_id]:
raise ValueError("unexpected Light[%d].Clear" % row_id)
light_by_id[row_id][7] = new_param
start_param = max(
max(params_by_id),
(max(int_by_id) + 17) // 18,
(max(float_by_id) + 5) // 6,
) + 1
if start_param != 1139:
raise ValueError(
"winner-derived clone start is %d, expected 1139; refusing an unsafe merge"
% start_param
)
for source_param, new_param, skybox_id in ((23, 1139, 8), (21, 1140, 9)):
clone = params_by_id[source_param][:]
clone[0] = new_param
clone[2] = skybox_id
params.append(clone)
for per, source, destination in (
(18, int_by_id, ints),
(6, float_by_id, floats),
):
for band in range(per):
source_id = band_row_id(source_param, band, per)
target_id = band_row_id(new_param, band, per)
if source_id not in source or target_id in source:
raise ValueError("unsafe band clone %d -> %d" % (source_id, target_id))
row = source[source_id][:]
row[0] = target_id
destination.append(row)
if params_by_id.get(269, [None, None, None])[2] != 0:
raise ValueError("unexpected LightParams[269].LightSkybox")
params_by_id[269][2] = 10
strings = bytearray(sky_strings)
extended = [[row[0], row[1], 0, 0] for row in skies]
specs = (
(8, r"Environments\Stars\10can_volcanicsky01.mdx", 15, ""),
(9, r"Environments\Stars\bonewastesskybox.mdx", 4, ""),
(
10,
r"Environments\Stars\8drk_darkshoreelune_sky01.mdx",
2,
r"Environments\Stars\hyjal_skybox.mdx",
),
)
for row_id, model, flags, celestial in specs:
extended.append(
[row_id, append_string(strings, model), flags, append_string(strings, celestial)]
)
light.sort(key=lambda row: row[0])
params.sort(key=lambda row: row[0])
ints.sort(key=lambda row: row[0])
floats.sort(key=lambda row: row[0])
extended.sort(key=lambda row: row[0])
outputs = {
"Light.dbc": pack_dbc(light, light_strings, 12),
"LightParams.dbc": pack_dbc(params, params_strings, 9),
"LightIntBand.dbc": pack_dbc(ints, int_strings, 34),
"LightFloatBand.dbc": pack_dbc(floats, float_strings, 34),
"LightSkybox.dbc": pack_dbc(extended, bytes(strings), 4),
}
report = {
"source_rows": {
name: len(parse_dbc(winners[name], name)[0]) for name in REQUIRED
},
"output_rows": {
name: len(parse_dbc(payload, name)[0]) for name, payload in outputs.items()
},
"changed_light_rows": sorted(targets),
"light_14_clear": 16,
"clone_params": [1139, 1140],
}
return outputs, report
def check_bands(int_payload: bytes, float_payload: bytes) -> None:
for label, payload, per in (
("LightIntBand", int_payload, 18),
("LightFloatBand", float_payload, 6),
):
records, _, _ = parse_dbc(payload, label)
rows = indexed(records, label)
for source_param, target_param in ((23, 1139), (21, 1140)):
for band in range(per):
source_id = band_row_id(source_param, band, per)
target_id = band_row_id(target_param, band, per)
if target_id not in rows or rows[source_id][1:] != rows[target_id][1:]:
raise ValueError("%s clone mismatch at row %d" % (label, target_id))
top = band_row_id(1140, per - 1, per)
stray = [row_id for row_id in rows if row_id > top]
if stray:
raise ValueError("%s has rows above param 1140 span" % label)