89 lines
3.6 KiB
Python
89 lines
3.6 KiB
Python
"""Prepare original Alien Grunt / hornet assets, without changing Steam files.
|
|
|
|
Embed the companion texture MDL; keep only self-contained sequence-group-zero
|
|
animations. External group 1 contains cinematic sequences, not the combat set.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SOURCE = Path.home() / '.steam/steam/steamapps/common/Half-Life/valve'
|
|
DEST = ROOT / 'assets/boss'
|
|
|
|
|
|
def digest(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def prepare_model(name, companion=None):
|
|
src = SOURCE / 'models' / (name + '.mdl')
|
|
body = bytearray(src.read_bytes())
|
|
assert body[:4] == b'IDST' and struct.unpack_from('<i', body, 4)[0] == 10
|
|
sources = [{'path': 'models/' + src.name, 'sha256': digest(src)}]
|
|
if companion:
|
|
texpath = SOURCE / 'models' / companion
|
|
tex = bytearray(texpath.read_bytes())
|
|
base = len(body)
|
|
count, records, data, refs, families, skins = struct.unpack_from('<6i', tex, 180)
|
|
assert count > 0
|
|
for i in range(count):
|
|
pos = records + i * 80
|
|
pixel = struct.unpack_from('<i', tex, pos + 76)[0]
|
|
struct.pack_into('<i', tex, pos + 76, pixel + base)
|
|
struct.pack_into('<6i', body, 180, count, records + base, data + base,
|
|
refs, families, skins + base)
|
|
body.extend(tex)
|
|
sources.append({'path': 'models/' + companion, 'sha256': digest(texpath)})
|
|
count, offset = struct.unpack_from('<2i', body, 164)
|
|
sequences, omitted, used = [], [], set()
|
|
for i in range(count):
|
|
record = bytearray(body[offset + 176 * i:offset + 176 * (i + 1)])
|
|
label = bytes(record[:32]).split(b'\0')[0].decode('ascii')
|
|
group = struct.unpack_from('<i', record, 156)[0]
|
|
if group:
|
|
omitted.append({'index': i, 'label': label, 'external_group': group})
|
|
continue
|
|
unique = label
|
|
suffix = 2
|
|
while unique in used:
|
|
unique = label + '_' + str(suffix)
|
|
suffix += 1
|
|
used.add(unique)
|
|
record[:32] = unique.encode().ljust(32, b'\0')
|
|
sequences.append(record)
|
|
# All sequence offsets still address the original, unchanged body data.
|
|
struct.pack_into('<2i', body, 164, len(sequences), len(body))
|
|
body.extend(b''.join(sequences))
|
|
struct.pack_into('<i', body, 172, 1)
|
|
struct.pack_into('<i', body, 72, len(body))
|
|
out = DEST / (name + '_textured.mdl')
|
|
out.write_bytes(body)
|
|
return {'sources': sources, 'destination': str(out.relative_to(ROOT)),
|
|
'sha256': digest(out), 'animations': sorted(used), 'omitted_sequences': omitted}
|
|
|
|
|
|
def main():
|
|
DEST.mkdir(parents=True, exist_ok=True)
|
|
records = [prepare_model('agrunt', 'agruntt.mdl'), prepare_model('hornet')]
|
|
sounds = ['agrunt/ag_alert1.wav', 'agrunt/ag_attack1.wav', 'agrunt/ag_fire1.wav',
|
|
'agrunt/ag_idle1.wav', 'agrunt/ag_pain1.wav', 'agrunt/ag_die1.wav',
|
|
'ambience/port_suckin1.wav', 'ambience/port_suckout1.wav']
|
|
for relative in sounds:
|
|
src = SOURCE / 'sound' / relative
|
|
out = ROOT / 'assets/audio' / relative
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(src, out)
|
|
assert digest(src) == digest(out)
|
|
records.append({'source': 'sound/' + relative,
|
|
'destination': str(out.relative_to(ROOT)), 'sha256': digest(src)})
|
|
(DEST / 'source_manifest.json').write_text(json.dumps(records, indent=2) + '\n')
|
|
for r in records:
|
|
print(r.get('destination'), r['sha256'])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|