From 355b135d699c297059d5926f344ebdd4eb34a477 Mon Sep 17 00:00:00 2001 From: Jacob Babor Date: Fri, 19 Jun 2026 20:48:08 -0500 Subject: [PATCH] Fix traffic archive extraction workflow --- docs/traffic-system-debrief.md | 56 +++++++++++++--------- tools/build_lane_weight_archive.sh | 21 ++++++-- tools/extract_archive_segment.py | 77 ++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 28 deletions(-) create mode 100755 tools/extract_archive_segment.py diff --git a/docs/traffic-system-debrief.md b/docs/traffic-system-debrief.md index 3df10a5..0e8962c 100644 --- a/docs/traffic-system-debrief.md +++ b/docs/traffic-system-debrief.md @@ -129,36 +129,37 @@ The important flags for GPS weighting are `Road`, `Intersection`, `GPSOnly`, and ## Observed Lane Distribution -The local EP1 `all.traffic_persistent` export contains `31,717` lanes. +The local EP1 `all.traffic_persistent` resource contains `33,952` lanes when +extracted from the archive's raw compressed segment. Observed `maxSpeed` distribution: ```text all lanes: - 1: 12284 - 15: 11638 - 10: 4675 - 17: 3013 + 1: 12852 + 15: 12107 + 10: 5859 + 17: 3027 14: 96 8: 8 9: 2 4: 1 highway lanes: - 17: 2688 + 17: 2700 15: 164 1: 2 road lanes: - 15: 11474 - 10: 495 - 17: 325 + 15: 11943 + 10: 671 + 17: 327 14: 96 8: 4 pavement lanes: - 1: 12280 - 10: 4180 + 1: 12848 + 10: 5188 8: 4 9: 2 4: 1 @@ -172,10 +173,10 @@ defaults: - highway speed target: `25` - road speed floor: `15` -That changes `2,506` lanes: +That changes `2,519` lanes: -- `2,419` highway boosts -- `87` road floor boosts +- `2,429` highway boosts +- `90` road floor boosts It leaves pavement/crosswalk/blocked/disabled lanes alone. @@ -226,13 +227,21 @@ already consumes. The working flow is: -1. Extract `all.traffic_persistent` with WolvenKit CLI. -2. Serialize the CR2W resource to JSON. -3. Patch lane `maxSpeed` values. -4. Deserialize the patched JSON back to CR2W. -5. Put the CR2W back under the original resource path. -6. Pack an archive. -7. Place the archive in `archive/pc/mod`. +1. Read the archive index and extract the exact compressed segment for resource + hash `3419764573789342681`. +2. Decompress that KARK segment with WolvenKit CLI's Oodle command. +3. Serialize the CR2W resource to JSON. +4. Patch lane `maxSpeed` values. +5. Deserialize the patched JSON back to CR2W. +6. Put the CR2W back under the original resource path. +7. Pack an archive. +8. Place the archive in `archive/pc/mod`. + +Do not use plain `cp77tools extract` for this resource. In this install, +WolvenKit CLI extracted a `9,984,106` byte CR2W, while the stock archive segment +declares and decompresses to `10,673,648` bytes. A vanilla control archive built +from the smaller extraction crashed during load. A vanilla control archive built +from the raw segment reached the game and the GPS worked normally. The generated archive currently contains exactly: @@ -249,8 +258,9 @@ and traffic simulation, the patch may also affect some traffic behavior on highways. The Oodle library was not available inside the toolbox, so WolvenKit packed with -its Kraken fallback. WolvenKit successfully round-tripped and listed the archive, -but in-game testing is still required. +its Kraken fallback. A vanilla full-resource control archive loaded in game and +GPS worked normally, so the earlier crash was caused by bad extracted data rather +than the Kraken-packed archive container. The patch targets the EP1 traffic resource because Phantom Liberty is installed. On a non-PL install, the basegame archive resource would need to be patched diff --git a/tools/build_lane_weight_archive.sh b/tools/build_lane_weight_archive.sh index a9e1a2e..ad54b98 100755 --- a/tools/build_lane_weight_archive.sh +++ b/tools/build_lane_weight_archive.sh @@ -21,13 +21,24 @@ pack_root="${work_dir}/${mod_name}" packed_dir="${work_dir}/05_packed" resource_dir=$(dirname "${resource_path}") resource_file=$(basename "${resource_path}") +resource_hash=3419764573789342681 -mkdir -p "${raw_dir}" "${json_dir}" "${patched_json_dir}" "${patched_raw_dir}" "${pack_root}/${resource_dir}" "${packed_dir}" +mkdir -p "${raw_dir}/${resource_dir}" "${json_dir}" "${patched_json_dir}" "${patched_raw_dir}" "${pack_root}/${resource_dir}" "${packed_dir}" -tools/cp77_toolbox.sh extract "${archive_path}" \ - --gamepath "${game_dir}" \ - --outpath "${raw_dir}" \ - --regex "${resource_regex}" +if [[ -f "${archive_path}" ]]; then + kark_path="${raw_dir}/${resource_file}.kark" + decompressed_base="${raw_dir}/${resource_file}.raw" + python3 tools/extract_archive_segment.py "${archive_path}" "${resource_hash}" "${kark_path}" + tools/cp77_toolbox.sh oodle decompress "${kark_path}" "${decompressed_base}" || true + if [[ ! -f "${decompressed_base}.bin" ]]; then + echo "failed to decompress ${kark_path}" >&2 + exit 1 + fi + mv "${decompressed_base}.bin" "${raw_dir}/${resource_path}" +else + echo "raw segment extraction needs a single archive file, got: ${archive_path}" >&2 + exit 2 +fi tools/cp77_toolbox.sh convert serialize "${raw_dir}" --outpath "${json_dir}" diff --git a/tools/extract_archive_segment.py b/tools/extract_archive_segment.py new file mode 100755 index 0000000..f7e4e70 --- /dev/null +++ b/tools/extract_archive_segment.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Extract a single compressed RED4 archive segment by resource hash.""" + +from __future__ import annotations + +import argparse +import struct +from pathlib import Path + + +FILE_ENTRY_SIZE = 56 +FILE_SEGMENT_SIZE = 16 + + +def parse_hash(value: str) -> int: + return int(value, 0) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("archive", type=Path) + parser.add_argument("resource_hash", type=parse_hash) + parser.add_argument("outpath", type=Path) + args = parser.parse_args() + + with args.archive.open("rb") as archive: + header = archive.read(40) + if len(header) != 40: + raise SystemExit("archive header is too short") + magic, _version, index_pos, _index_size, *_rest = struct.unpack("