Initial commit: ArmADump ammo/armor rebalance tool

- SQF extraction scripts (ammo, armor, weapons, magazines) with ACE3 properties
- Python pipeline: extract_csvs, split_by_mod, generate_patches
- Generates CfgPatches config.cpp normalizing non-RHS values to RHS baseline
- Documentation: REFERENCE.md, GENERATE_PATCHES_EXPLAINED.md, AGENTS.md
This commit is contained in:
Samuele Lorefice
2026-07-20 18:27:33 +02:00
commit 83d2cb6c52
11 changed files with 2194 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
"""
extract_csvs.py — Extract CSV data from Arma 3 .rpt log file.
Parses the .rpt log and extracts CSV blocks delimited by
--- ... START --- / --- ... END --- markers.
Usage:
python scripts/extract_csvs.py path/to/arma3.rpt
python scripts/extract_csvs.py path/to/arma3.rpt --output-dir ./extracted
Output files (in repo root by default):
ammo_raw.csv
armor_raw.csv
weapons_raw.csv
magazines_raw.csv
"""
import argparse
import re
import sys
from pathlib import Path
# Arma 3 .rpt lines start with "HH:MM:SS " timestamp prefix
RPT_TIMESTAMP_RE = re.compile(r'^\d{2}:\d{2}:\d{2}\s+')
# Marker patterns -> output filename
MARKERS = {
"AMMO EXPORT CSV": "ammo_raw.csv",
"ARMOR EXPORT CSV": "armor_raw.csv",
"WEAPONS EXPORT CSV": "weapons_raw.csv",
"MAGAZINES EXPORT CSV": "magazines_raw.csv",
}
# Also detect the old sheet-style markers for backwards compat
LEGACY_MARKERS = {
"CONFIG DUMP CSV SHEET 3 (AMMO)": "ammo_raw.csv",
"CONFIG DUMP SHEET 1": "weapons_raw.csv",
"CONFIG DUMP SHEET 2": "armor_raw.csv",
}
# Header line prefix to skip (the diag_log text("name;...") header)
HEADER_PREFIXES = ["name;", "name ", "source_mod;"]
def extract_from_rpt(rpt_path: Path, output_dir: Path) -> dict[str, int]:
"""Parse .rpt file and extract CSV blocks.
Returns dict of filename -> number of data rows extracted.
"""
if not rpt_path.exists():
print(f"Error: {rpt_path} not found")
sys.exit(1)
print(f"Reading: {rpt_path}")
print(f"File size: {rpt_path.stat().st_size / 1024 / 1024:.1f} MB")
# Read the full file
with open(rpt_path, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
print(f"Total lines: {len(lines)}")
# Find all START/END marker positions
# Format: --- AMMO EXPORT CSV START ---
# --- AMMO EXPORT CSV END ---
start_pattern = re.compile(r'^\s*---\s+(.+?)\s+START\s+---\s*$')
end_pattern = re.compile(r'^\s*---\s+(.+?)\s+END\s+---\s*$')
# Build list of (start_line, end_line, marker_name)
blocks = []
active_block = None
for i, line in enumerate(lines):
# Strip RPT timestamp prefix (e.g. "17:55:18 ")
line_clean = RPT_TIMESTAMP_RE.sub('', line).strip()
# Strip surrounding quotes (.rpt wraps diag_log strings in "...")
if line_clean.startswith('"') and line_clean.endswith('"'):
line_clean = line_clean[1:-1].strip()
start_match = start_pattern.match(line_clean)
if start_match:
marker = start_match.group(1).strip()
active_block = {"marker": marker, "start": i + 1, "end": None}
continue
end_match = end_pattern.match(line_clean)
if end_match:
marker = end_match.group(1).strip()
if active_block and active_block["marker"] == marker:
active_block["end"] = i # end line (exclusive)
blocks.append(active_block)
active_block = None
print(f"Found {len(blocks)} CSV blocks in .rpt")
# Map marker names to output filenames
marker_to_file = {}
for marker_name, filename in {**MARKERS, **LEGACY_MARKERS}.items():
marker_to_file[marker_name] = filename
# Extract each block
results = {}
output_dir.mkdir(parents=True, exist_ok=True)
for block in blocks:
marker = block["marker"]
filename = marker_to_file.get(marker)
if not filename:
# Try partial match
for key, fname in marker_to_file.items():
if key in marker:
filename = fname
break
if not filename:
print(f" Skipping unrecognized marker: {marker}")
continue
# Extract lines between START and END (exclusive of marker lines)
data_lines = []
header_found = False
for line in lines[block["start"]:block["end"]]:
# Strip RPT timestamp prefix
stripped = RPT_TIMESTAMP_RE.sub('', line).strip()
# Strip surrounding quotes
if stripped.startswith('"') and stripped.endswith('"'):
stripped = stripped[1:-1].strip()
# Skip empty lines
if not stripped:
continue
# Remove diag_log wrapper if present: text("...") -> ...
cleaned = stripped
if cleaned.startswith('text("') and cleaned.endswith('")'):
cleaned = cleaned[6:-2]
elif cleaned.startswith("text('") and cleaned.endswith("')"):
cleaned = cleaned[6:-2]
# Keep only the first header row (diag_log text("name;..."))
if any(cleaned.startswith(p) for p in HEADER_PREFIXES):
if not header_found:
data_lines.append(cleaned)
header_found = True
continue
data_lines.append(cleaned)
out_path = output_dir / filename
with open(out_path, "w", encoding="utf-8", newline="") as f:
for line in data_lines:
f.write(line + "\n")
results[filename] = len(data_lines)
print(f" {filename:25s} -> {len(data_lines):6d} rows ({out_path})")
return results
def main():
parser = argparse.ArgumentParser(
description="Extract CSV data from Arma 3 .rpt log file")
parser.add_argument(
"rpt_file",
help="Path to Arma 3 .rpt log file")
parser.add_argument(
"--output-dir", "-o",
default=".",
help="Output directory for CSV files (default: repo root)")
args = parser.parse_args()
rpt_path = Path(args.rpt_file).resolve()
output_dir = Path(args.output_dir).resolve()
print("=" * 60)
print("ArmADump - Extract CSVs from .rpt log")
print("=" * 60)
results = extract_from_rpt(rpt_path, output_dir)
print(f"\n{'=' * 60}")
if results:
print(f"Extracted {len(results)} CSV files to {output_dir}")
for fname, count in sorted(results.items()):
print(f" {fname}: {count} rows")
print("\nNext step: python scripts/split_by_mod.py")
else:
print("No CSV blocks found in .rpt file!")
print("Make sure the extraction scripts were run in Arma first.")
print("=" * 60)
if __name__ == "__main__":
main()