Embed RHS 1.9 compatibility; record Workshop 0.0.141
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-05 16:39:35 +01:00
parent 9418b6950a
commit 9caed3f12d
325 changed files with 76722 additions and 25 deletions
+74
View File
@@ -0,0 +1,74 @@
# RHS 1.9 compatibility
## Embedded in Escape
The same two replacements can live directly in Escape. Prepare them before
opening Workbench, testing, or packaging a fresh checkout:
```sh
python3 compatibility/rhs-1.9/prepare.py \
--rhs '/path/to/addons/RHS-StatusQuo_595F2BF2F44836FB' \
--mission addons/core
```
This keeps Escape's project identity and existing RHS dependency unchanged.
No standalone compatibility addon is required. The generated scripts are ignored
by Git but included by Workbench in Escape's package. Installed RHS files stay
untouched. The command validates both upstream hashes, refuses to overwrite local
edits, and can be repeated for identical inputs. A new RHS version requires review
and removal or regeneration of both replacements; they must not mask upstream fixes.
Credit Red Hammer Studios and link its EULA in Escape's Workshop description.
The RHS derivative terms require public, noncommercial distribution under a
similar non-derivative license. Do not redistribute generated RHS source in Git
or development-source downloads. The separate WCS patch is not embedded here.
The embedded layout passed native compilation and normal mission startup through
player spawn on 1.9.0.37. Native packing succeeded and both replacements match
the inputs inside Escape's PAK. See the [embedded test evidence](../../docs/evidence/rhs-1.9-embedded-2026-11-05/README.md).
It was published inside Escape **0.0.141** after successful Workshop upload and
backend processing. Inherited dependency diagnostics remain.
## Standalone experiment
Separate addon for RHS Status Quo **0.16.5237** and Reforger **1.9.0.37**.
It uses same-path script replacement to fix declarations before compilation:
- Accept and forward `isGM` in `RHS_CAS_EditableEntityComponent.SetAuthor`.
- Use `WheelSlotInfo.GetWheelIndex()` instead of the removed wheel-index field.
`prepare.py` reads the two scripts from the installed RHS package, verifies their
exact hashes, and generates the patched addon in a new local directory. It never
changes installed Workshop files. This directory contains the patch recipe and
project definition; generated RHS source stays in ignored local storage.
From the repository root:
```sh
python3 compatibility/rhs-1.9/prepare.py \
--rhs '/path/to/addons/RHS-StatusQuo_595F2BF2F44836FB' \
--output '.local/rhs-19-addon'
```
For an isolated test, add dependency `E119F1C05A274B83` to the test copy of
Escape's `addon.gproj`, and expose the generated addon through its addon search
directory. Keep the original RHS dependency. The GUID is a local project identity,
not a published Workshop ID.
Both changes pass native compilation of GameLib, Game and WorkbenchGame with
Escape's complete RHS/WCS/BMP/Parachute dependency graph on 1.9.0.37. The current
scenario prepares and spawns the player. The six-faction rescue fixture completes
114 assertions with no assertion failures or script exceptions: faction-matched
crews and cargo capacity, two physical landings, wounded boarding, and loaded
airborne departure. This is local Workbench evidence, not multiplayer or general
vehicle-handling qualification. Both replaced scripts are byte-identical in the
native packed output. No Workshop publication has been performed.
The full logs also exposed a WCS BMP tire initialization failure. A separate
local WCS experiment is not included in the published mission. Other inherited
resource/property diagnostics remain in the logs; the strict all-errors report
is not clean. See [native evidence](../../docs/evidence/rhs-1.9-compatibility-2026-11-05/report.json).
References: [Bohemia's script replacement documentation](https://community.bistudio.com/wiki/Arma_Reforger:Data_Modding_Basics)
and [RHS derivative-content terms](https://docs.rhsmods.org/rhs-status-quo-user-documentation/arma-reforger/rhs-status-quo/eula).
Workshop publication requires review of the generated files and these terms.
+9
View File
@@ -0,0 +1,9 @@
GameProject {
ID "ERR_RHS19Fix"
GUID "E119F1C05A274B83"
TITLE "RHS 1.9 Compatibility"
Dependencies {
"58D0FB3206B6F859"
"595F2BF2F44836FB"
}
}
@@ -0,0 +1,7 @@
Includes an unofficial RHS Status Quo 0.16.5237 script compatibility patch for
Arma Reforger 1.9.0.37. The original RHS addon and content packs remain required;
no separate RHS compatibility addon is needed.
Original RHS scripts and content are by Red Hammer Studios. This mission and
its compatibility changes are not an official RHS release.
RHS terms: https://docs.rhsmods.org/rhs-status-quo-user-documentation/arma-reforger/rhs-status-quo/eula
+147
View File
@@ -0,0 +1,147 @@
#!/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
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())
if __name__ == '__main__':
main()
@@ -0,0 +1,17 @@
RHS 1.9 Script Compatibility
Unofficial compatibility patch for RHS Status Quo 0.16.5237 on Arma Reforger 1.9.0.37.
Requires the original RHS Status Quo addon and its content packs.
Updates the CAS editor component's SetAuthor signature and the tire component's
wheel-index accessor so the installed RHS scripts compile on 1.9. It does not
retune vehicle handling or add support for every new 1.9 feature.
Original RHS scripts and content are by Red Hammer Studios. This is a community
compatibility addon and is not an official RHS release.
RHS terms: https://docs.rhsmods.org/rhs-status-quo-user-documentation/arma-reforger/rhs-status-quo/eula
Built specifically for the versions above. Reassess or remove this patch when
RHS publishes its own compatible update, because these replacements can otherwise
hide newer upstream script changes.