Simplify content controls and add vehicle roles; record Workshop 0.0.144
continuous-integration/drone/push Build is passing

Discover installed mod vehicles and save independent garage, traffic, patrol and convoy assignments. Retire the superseded RHS 1.9 overrides. Include native menu, crew/driving and publication evidence.
This commit is contained in:
2026-11-06 22:06:38 +01:00
parent 518a1a5744
commit 460d9808e5
116 changed files with 14244 additions and 404 deletions
+5 -139
View File
@@ -1,146 +1,12 @@
#!/usr/bin/env python3
"""Generate a local RHS 0.16.5237 compatibility addon from installed scripts.
Only two pinned scripts are read; installed Workshop files are never modified.
The generated addon replaces scripts at their original paths before compilation.
"""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
import struct
import sys
import zlib
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parents[1] / 'tools'))
from pak_inventory import Limits, parse_tree
CAS = 'Scripts/Game/Editor/Components/EditableEntity/RHS_CAS_EditableEntityComponent.c'
TIRE = 'Scripts/Game/Vehicle/RHS_DestructionTireComponent.c'
EXPECTED = {
CAS: '1b4c5c8d3078f60f617cab94457bd0d9c96a048ad05117353e67e22b9972af37',
TIRE: '9fc9fffa3e63a1115e174e51faa222504038e57790f95c57ef691da8745039c9',
}
def replace_once(text, old, new):
if text.count(old) != 1:
raise ValueError('Expected exactly one patch location: ' + old)
return text.replace(old, new)
def patched(path, raw):
if hashlib.sha256(raw).hexdigest() != EXPECTED[path]:
raise ValueError('RHS script changed; review compatibility before patching: ' + path)
text = raw.decode('utf-8').replace('\r\n', '\n')
if path == CAS:
text = replace_once(text, 'override void SetAuthor(int playerID)',
'override void SetAuthor(int playerID, bool isGM = false)')
text = replace_once(text, 'super.SetAuthor(playerID);\n\n\t\tRHS_CAS_',
'super.SetAuthor(playerID, isGM);\n\n\t\tRHS_CAS_')
elif path == TIRE:
text = replace_once(text, 'return m_iWheelIndex;', 'return GetWheelIndex();')
return text.encode('utf-8')
def read_scripts(folder):
project = folder / 'addon.gproj'
expected_project = '51fda0afde5ebcd4dc88c6c9451d960b67aceaee437f02f1cc80aef724e3779c'
if hashlib.sha256(project.read_bytes()).hexdigest() != expected_project:
raise ValueError('Expected the audited RHS Status Quo 0.16.5237 project')
found = {}
for pak in sorted(folder.glob('*.pak')):
with pak.open('rb') as stream:
header = stream.read(12)
if header[:4] != b'FORM' or header[8:] != b'PAC1':
raise ValueError('Unsupported archive: ' + str(pak))
offset, chunks = 12, {}
for expected in (b'HEAD', b'DATA', b'FILE'):
stream.seek(offset)
tag, size = struct.unpack('>4sI', stream.read(8))
if tag != expected or offset + 8 + size > pak.stat().st_size:
raise ValueError('Invalid archive chunks')
chunks[tag] = (offset + 8, size)
offset += 8 + size
start, size = chunks[b'FILE']
if size > 32 * 1024**2:
raise ValueError('Archive tree too large')
stream.seek(start)
data_start, data_size = chunks[b'DATA']
entries, _, _ = parse_tree(stream.read(size), data_start,
data_start + data_size, Limits(total_bytes=32 * 1024**3))
for entry in entries:
path = entry['path']
if path not in EXPECTED:
continue
if path in found or entry['content_bytes'] > 1024**2:
raise ValueError('Duplicate or oversized target script')
stream.seek(entry['offset'])
raw = stream.read(entry['stored_bytes'])
if entry['compression'] == 'zlib':
raw = zlib.decompress(raw)
found[path] = raw
if set(found) != set(EXPECTED):
raise ValueError('Missing expected RHS scripts')
return found
"""Retired entry point: updated RHS supplies the 1.9 fixes itself."""
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--rhs', type=Path, required=True, help='Installed RHS Status Quo directory')
destination = parser.add_mutually_exclusive_group(required=True)
destination.add_argument('--output', type=Path, help='New standalone addon directory')
destination.add_argument('--mission', type=Path, help='Existing Escape addon directory to embed the fixes in')
args = parser.parse_args()
if args.mission:
mission = args.mission.resolve()
if mission.is_relative_to(args.rhs.resolve()):
parser.error('Mission must be outside the installed RHS directory')
project = (mission / 'addon.gproj').read_text()
if 'GUID "2528466DE220D91C"' not in project or '"595F2BF2F44836FB"' not in project:
parser.error('Expected the Escape project with its original RHS dependency')
if '"E119F1C05A274B83"' in project:
parser.error('Remove the standalone RHS fix dependency before embedding it')
originals = read_scripts(args.rhs)
outputs = {path: patched(path, raw) for path, raw in originals.items()}
# Validate all destinations before writing; never overwrite a local edit
# or follow a link into another addon. Repeating the same patch is safe.
for path, raw in outputs.items():
target = mission / path
if any(parent.is_symlink() for parent in [target, *target.parents] if parent != mission.parent):
parser.error('Symlink in mission destination: ' + path)
if target.exists() and (not target.is_file() or target.read_bytes() != raw):
parser.error('Existing mission script differs; preserve and review it: ' + path)
for path, raw in outputs.items():
target = mission / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(raw)
print('Embedded both pinned RHS fixes in ' + str(mission))
return
if args.output.exists():
parser.error('Output already exists; choose a new directory to preserve prior work')
if args.output.resolve().is_relative_to(args.rhs.resolve()):
parser.error('Output must be outside the installed RHS directory')
report_path = args.output.with_name(args.output.name + '-patch-inputs.json')
if report_path.exists():
parser.error('Output provenance already exists; choose a new directory')
# Validate every input and patch before creating any output.
originals = read_scripts(args.rhs)
outputs = {path: patched(path, raw) for path, raw in originals.items()}
args.output.mkdir(parents=True)
shutil.copy2(HERE / 'addon.gproj', args.output / 'addon.gproj')
report = {'rhs_version': '0.16.5237', 'target_game_version': '1.9.0.37',
'native_validation': 'required', 'scripts': {}}
for path, raw in outputs.items():
target = args.output / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(raw)
report['scripts'][path] = {'original_sha256': EXPECTED[path],
'patched_sha256': hashlib.sha256(raw).hexdigest()}
report_path.write_text(json.dumps(report, indent=2) + '\n')
print(args.output.resolve())
raise SystemExit(
"Retired: use updated RHS Status Quo. Remove the old RHS 1.9 "
"compatibility addon/overrides; do not regenerate them."
)
if __name__ == '__main__':