Files
Samuele Lorefice 83d2cb6c52 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
2026-07-20 18:27:33 +02:00

187 lines
5.1 KiB
Python

"""
split_by_mod.py — Split raw Arma 3 CSV exports into per-mod folders.
Reads raw CSV files (extracted from .rpt log) and organizes them by source mod.
Each raw CSV has a 'source_mod' column (first column) that identifies which
CfgPatches class defined that entry.
Usage:
python scripts/split_by_mod.py
Input files (in repo root):
ammo_raw.csv — from ammo.sqf
armor_raw.csv — from armor.sqf
weapons_raw.csv — from weapons.sqf
magazines_raw.csv — from magazines.sqf
Output structure:
data/
├── _rhs/
│ ├── ammo.csv
│ ├── armor.csv
│ ├── weapons.csv
│ ├── magazines.csv
├── _vanilla/
│ ├── ammo.csv
│ ├── ...
├── [mod_name]/
│ ├── ammo.csv
│ ├── ...
"""
import csv
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = REPO_ROOT / "data"
# Map (input_filename, output_filename)
CSV_FILES = [
("ammo_raw.csv", "ammo.csv"),
("armor_raw.csv", "armor.csv"),
("weapons_raw.csv", "weapons.csv"),
("magazines_raw.csv", "magazines.csv"),
]
# Known CfgPatches -> folder name mappings
# Prefix with _ for baseline mods (rhs, vanilla) so they sort first
# Order matters: more specific prefixes first
MOD_FOLDER_PREFIXES = [
# RHS family
("rhs_", "_rhs"),
("rhsusf_", "_rhsusf"),
("rhsgref_", "_rhsgref"),
("rhssaf_", "_rhssaf"),
# Vanilla
("A3_", "_vanilla"),
("A3a3_", "_vanilla"),
# ACE
("ace_", "_ace"),
# CBA
("cba_", "_cba"),
]
# Exact matches for edge cases
MOD_FOLDER_EXACT = {
"ace_main": "_ace",
"ace_ballistics": "_ace",
"cba_common": "_cba",
"cba_xeh": "_cba",
}
def sanitize_folder_name(mod_name: str) -> str:
"""Convert a CfgPatches class name to a safe folder name.
Uses prefix matching for known mod families, then falls back to
extracting the first meaningful segment.
"""
# Exact match first
if mod_name in MOD_FOLDER_EXACT:
return MOD_FOLDER_EXACT[mod_name]
# Prefix matching for known families
for prefix, folder in MOD_FOLDER_PREFIXES:
if mod_name.startswith(prefix):
return folder
# Heuristic: take the first segment before underscore,
# stripping common suffixes
parts = mod_name.lower().split("_")
suffixes = {"main", "weapons", "data", "config", "core", "compat",
"f", "e", "c", "add", "addons"}
clean = [p for p in parts if p not in suffixes and p]
if clean:
return clean[0]
return mod_name.lower()
def parse_rpt_csv(filepath: Path) -> list[list[str]]:
"""Parse a semicolon-delimited CSV file extracted from .rpt log.
Handles lines that may have been wrapped or have trailing semicolons.
"""
rows = []
if not filepath.exists():
print(f" Warning: {filepath} not found, skipping")
return rows
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
for line in f:
line = line.strip()
if not line:
continue
# Split on semicolons, strip whitespace
parts = [p.strip() for p in line.split(";")]
# Remove trailing empty fields (from trailing semicolons)
while parts and parts[-1] == "":
parts.pop()
if parts:
rows.append(parts)
return rows
def split_csv(input_name: str, output_name: str):
"""Split a single raw CSV file into per-mod files."""
input_path = REPO_ROOT / input_name
print(f"\nProcessing {input_name}...")
rows = parse_rpt_csv(input_path)
if not rows:
return
# First row is header
header = rows[0]
data_rows = rows[1:]
# source_mod is always the first column
mod_groups = defaultdict(list)
for row in data_rows:
if len(row) < 2:
continue
mod_name = row[0].strip().strip('"')
folder = sanitize_folder_name(mod_name)
mod_groups[folder].append(row)
print(f" Found {len(mod_groups)} unique mods, {len(data_rows)} total entries")
# Write per-mod files
for folder, group_rows in sorted(mod_groups.items()):
mod_dir = DATA_DIR / folder
mod_dir.mkdir(parents=True, exist_ok=True)
out_path = mod_dir / output_name
with open(out_path, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f, delimiter=";")
writer.writerow(header)
writer.writerows(group_rows)
print(f" {folder:30s} -> {len(group_rows):5d} entries ({out_path.relative_to(REPO_ROOT)})")
def main():
print("=" * 60)
print("ArmADump - Split CSVs by Mod")
print("=" * 60)
print(f"Repository root: {REPO_ROOT}")
print(f"Output directory: {DATA_DIR}")
DATA_DIR.mkdir(parents=True, exist_ok=True)
for input_name, output_name in CSV_FILES:
split_csv(input_name, output_name)
print("\n" + "=" * 60)
print("Done! Per-mod CSVs written to data/")
print("=" * 60)
if __name__ == "__main__":
main()