Embed RHS 1.9 compatibility; record Workshop 0.0.141
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user