Per-mod output, packaging script, fix requiredAddons CfgPatches names
- Split single config.cpp into per-mod output/<mod>/config.cpp files
- Use original CfgPatches class names (not sanitized folders) in requiredAddons
- Skip 'unknown' source_mod from requiredAddons
- Add scripts/package_mod.py with armake2 PBO packing
- Add package_mod.ini for configurable mod packaging
- Use {} array syntax instead of [] for Arma config compatibility
This commit is contained in:
+85
-73
@@ -2,14 +2,14 @@
|
||||
generate_patches.py — Generate CfgPatches config.cpp for ammo/armor rebalance.
|
||||
|
||||
Reads per-mod CSVs from data/, compares to RHS baseline, and generates
|
||||
a config.cpp that overrides non-RHS values to match RHS equivalents.
|
||||
per-mod config.cpp files that override non-RHS values to match RHS equivalents.
|
||||
|
||||
Usage:
|
||||
python scripts/generate_patches.py
|
||||
|
||||
Output:
|
||||
output/config.cpp — The rebalance mod
|
||||
output/unmatched.csv — Items with no RHS equivalent (for manual review)
|
||||
output/<mod>/config.cpp — One rebalance config per mod
|
||||
output/unmatched.csv — Items with no RHS equivalent (for manual review)
|
||||
"""
|
||||
|
||||
import csv
|
||||
@@ -589,84 +589,75 @@ def format_number(val) -> str:
|
||||
return str(val)
|
||||
|
||||
|
||||
def generate_config_cpp(
|
||||
ammo_overrides: dict[str, list[AmmoOverride]],
|
||||
armor_overrides: dict[str, list[ArmorOverride]],
|
||||
source_mods: set[str],
|
||||
def generate_mod_config_cpp(
|
||||
mod: str,
|
||||
ammo_overrides: list[AmmoOverride],
|
||||
armor_overrides: list[ArmorOverride],
|
||||
source_mods: list[str] = None,
|
||||
) -> str:
|
||||
"""Generate the config.cpp content."""
|
||||
"""Generate config.cpp content for a single mod."""
|
||||
safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', mod)
|
||||
# Use original CfgPatches class names for requiredAddons, skip bogus ones
|
||||
skip_deps = {"unknown", ""}
|
||||
deps = [d for d in (source_mods if source_mods else [mod]) if d not in skip_deps]
|
||||
deps_str = ", ".join(f'"{d}"' for d in sorted(deps))
|
||||
lines = []
|
||||
lines.append('// ArmADump - Rebalance Config')
|
||||
lines.append(f'// Mod: {mod}')
|
||||
lines.append('// Auto-generated by generate_patches.py')
|
||||
lines.append('// Compare to RHS baseline values')
|
||||
lines.append('//')
|
||||
lines.append('// WARNING: Review output/unmatched.csv for items without RHS equivalents')
|
||||
lines.append('')
|
||||
|
||||
# CfgPatches
|
||||
lines.append('class CfgPatches {')
|
||||
for mod in sorted(source_mods):
|
||||
safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', mod)
|
||||
lines.append(f' class armadump_{safe_name} {{')
|
||||
lines.append(f' units[] = {{}};')
|
||||
lines.append(f' weapons[] = {{}};')
|
||||
lines.append(f' requiredAddons[] = {{"{mod}"}};')
|
||||
lines.append(f' }};')
|
||||
lines.append(f' class armadump_{safe_name} {{')
|
||||
lines.append(f' units[] = {{}};')
|
||||
lines.append(f' weapons[] = {{}};')
|
||||
lines.append(f' requiredAddons[] = {{{deps_str}}};')
|
||||
lines.append(f' }};')
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
|
||||
# CfgAmmo overrides
|
||||
has_ammo = any(v for v in ammo_overrides.values())
|
||||
if has_ammo:
|
||||
if ammo_overrides:
|
||||
lines.append('class CfgAmmo {')
|
||||
for mod in sorted(ammo_overrides.keys()):
|
||||
overrides = ammo_overrides[mod]
|
||||
if not overrides:
|
||||
continue
|
||||
for o in sorted(overrides, key=lambda x: x.classname):
|
||||
lines.append(f' class {o.classname} {{')
|
||||
for field_name, val in sorted(o.changes.items()):
|
||||
config_name = FIELD_TO_CONFIG.get(field_name, field_name)
|
||||
if isinstance(val, str) and val.startswith("["):
|
||||
# Array value — write as SQF array
|
||||
lines.append(f' {config_name}[] = {val};')
|
||||
else:
|
||||
lines.append(f' {config_name} = {format_number(val)};')
|
||||
lines.append(' };')
|
||||
for o in sorted(ammo_overrides, key=lambda x: x.classname):
|
||||
lines.append(f' class {o.classname} {{')
|
||||
for field_name, val in sorted(o.changes.items()):
|
||||
config_name = FIELD_TO_CONFIG.get(field_name, field_name)
|
||||
if isinstance(val, str) and val.startswith("["):
|
||||
sqf_val = val.replace("[", "{").replace("]", "}")
|
||||
lines.append(f' {config_name}[] = {sqf_val};')
|
||||
else:
|
||||
lines.append(f' {config_name} = {format_number(val)};')
|
||||
lines.append(' };')
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
|
||||
# CfgWeapons overrides (armor)
|
||||
has_armor = any(v for v in armor_overrides.values())
|
||||
if has_armor:
|
||||
if armor_overrides:
|
||||
lines.append('class CfgWeapons {')
|
||||
for mod in sorted(armor_overrides.keys()):
|
||||
overrides = armor_overrides[mod]
|
||||
if not overrides:
|
||||
continue
|
||||
for o in sorted(overrides, key=lambda x: x.classname):
|
||||
lines.append(f' class {o.classname} {{')
|
||||
lines.append(f' class ItemInfo {{')
|
||||
lines.append(f' class HitpointsProtectionInfo {{')
|
||||
for o in sorted(armor_overrides, key=lambda x: x.classname):
|
||||
lines.append(f' class {o.classname} {{')
|
||||
lines.append(f' class ItemInfo {{')
|
||||
lines.append(f' class HitpointsProtectionInfo {{')
|
||||
|
||||
# Group changes by body part
|
||||
part_changes = defaultdict(dict)
|
||||
for field_name, val in sorted(o.changes.items()):
|
||||
section, prop = ARMOR_FIELD_MAP[field_name]
|
||||
part_changes[section][prop] = val
|
||||
part_changes = defaultdict(dict)
|
||||
for field_name, val in sorted(o.changes.items()):
|
||||
section, prop = ARMOR_FIELD_MAP[field_name]
|
||||
part_changes[section][prop] = val
|
||||
|
||||
for part in ["Head", "Neck", "Chest", "Diaphragm", "Abdomen",
|
||||
"Body", "Arms", "Legs"]:
|
||||
if part in part_changes:
|
||||
lines.append(f' class {part} {{')
|
||||
for prop, val in sorted(part_changes[part].items()):
|
||||
lines.append(
|
||||
f' {prop} = {format_number(val)};')
|
||||
lines.append(f' }};')
|
||||
for part in ["Head", "Neck", "Chest", "Diaphragm", "Abdomen",
|
||||
"Body", "Arms", "Legs"]:
|
||||
if part in part_changes:
|
||||
lines.append(f' class {part} {{')
|
||||
for prop, val in sorted(part_changes[part].items()):
|
||||
lines.append(
|
||||
f' {prop} = {format_number(val)};')
|
||||
lines.append(f' }};')
|
||||
|
||||
lines.append(f' }};')
|
||||
lines.append(f' }};')
|
||||
lines.append(f' }};')
|
||||
lines.append(f' }};')
|
||||
lines.append(f' }};')
|
||||
lines.append(f' }};')
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
|
||||
@@ -726,7 +717,7 @@ def main():
|
||||
print("\nScanning non-baseline mods...")
|
||||
all_ammo_overrides = {}
|
||||
all_armor_overrides = {}
|
||||
all_source_mods = set()
|
||||
all_source_mods_per_folder = defaultdict(set)
|
||||
unmatched_ammo = []
|
||||
unmatched_armor = []
|
||||
|
||||
@@ -748,7 +739,8 @@ def main():
|
||||
ammo_ovs = find_ammo_overrides(mod_ammo, ammo_baseline)
|
||||
if ammo_ovs:
|
||||
all_ammo_overrides[mod_dir.name] = ammo_ovs
|
||||
all_source_mods.add(mod_dir.name)
|
||||
for o in ammo_ovs:
|
||||
all_source_mods_per_folder[mod_dir.name].add(o.source_mod)
|
||||
print(f" Ammo overrides: {len(ammo_ovs)}")
|
||||
|
||||
# Track unmatched
|
||||
@@ -761,7 +753,8 @@ def main():
|
||||
armor_ovs = find_armor_overrides(mod_armor, armor_baseline)
|
||||
if armor_ovs:
|
||||
all_armor_overrides[mod_dir.name] = armor_ovs
|
||||
all_source_mods.add(mod_dir.name)
|
||||
for o in armor_ovs:
|
||||
all_source_mods_per_folder[mod_dir.name].add(o.source_mod)
|
||||
print(f" Armor overrides: {len(armor_ovs)}")
|
||||
|
||||
for e in mod_armor:
|
||||
@@ -770,18 +763,37 @@ def main():
|
||||
unmatched_armor.append(e)
|
||||
|
||||
# Generate output
|
||||
print("\nGenerating config.cpp...")
|
||||
print("\nGenerating per-mod config files...")
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
config_cpp = generate_config_cpp(
|
||||
all_ammo_overrides, all_armor_overrides, all_source_mods)
|
||||
# Clean old output
|
||||
for child in OUTPUT_DIR.iterdir():
|
||||
if child.is_dir():
|
||||
for f in child.iterdir():
|
||||
f.unlink()
|
||||
child.rmdir()
|
||||
elif child.name != "unmatched.csv":
|
||||
child.unlink()
|
||||
|
||||
config_path = OUTPUT_DIR / "config.cpp"
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(config_cpp)
|
||||
print(f" Wrote {config_path}")
|
||||
mods_with_overrides = set()
|
||||
for mod in sorted(set(list(all_ammo_overrides.keys()) + list(all_armor_overrides.keys()))):
|
||||
mod_ammo = all_ammo_overrides.get(mod, [])
|
||||
mod_armor = all_armor_overrides.get(mod, [])
|
||||
if not mod_ammo and not mod_armor:
|
||||
continue
|
||||
|
||||
print("\nWriting unmatched items...")
|
||||
mod_dir = OUTPUT_DIR / mod
|
||||
mod_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
source_mods = sorted(all_source_mods_per_folder.get(mod, {mod}))
|
||||
config_cpp = generate_mod_config_cpp(mod, mod_ammo, mod_armor, source_mods)
|
||||
config_path = mod_dir / "config.cpp"
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(config_cpp)
|
||||
mods_with_overrides.add(mod)
|
||||
print(f" {mod}/config.cpp ({len(mod_ammo)} ammo, {len(mod_armor)} armor)")
|
||||
|
||||
print(f"\nWriting unmatched items...")
|
||||
write_unmatched(unmatched_ammo, unmatched_armor)
|
||||
|
||||
# Summary
|
||||
@@ -789,8 +801,8 @@ def main():
|
||||
total_armor = sum(len(v) for v in all_armor_overrides.values())
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"DONE - {total_ammo} ammo overrides, {total_armor} armor overrides")
|
||||
print(f" across {len(all_source_mods)} mods")
|
||||
print(f" Config: {config_path.relative_to(REPO_ROOT)}")
|
||||
print(f" across {len(mods_with_overrides)} mods")
|
||||
print(f" Output: {OUTPUT_DIR.relative_to(REPO_ROOT)}/<mod>/config.cpp")
|
||||
print(f" Unmatched: {REPO_ROOT / 'output' / 'unmatched.csv'}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user