- 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
187 lines
5.1 KiB
Python
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()
|