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:
@@ -51,11 +51,11 @@ Creates `data/[mod_name]/` folders with per-mod CSVs. Baseline mods (`_rhs`, `_v
|
|||||||
python scripts/generate_patches.py
|
python scripts/generate_patches.py
|
||||||
```
|
```
|
||||||
Produces:
|
Produces:
|
||||||
- `output/config.cpp` — CfgPatches config with overrides
|
- `output/<mod>/config.cpp` — Per-mod CfgPatches config with overrides
|
||||||
- `output/unmatched.csv` — Items with no RHS equivalent (review manually)
|
- `output/unmatched.csv` — Items with no RHS equivalent (review manually)
|
||||||
|
|
||||||
### Step 5: Load in Arma
|
### Step 5: Load in Arma
|
||||||
Drop `output/config.cpp` into an `@mod/addons/config.cpp` structure and load it.
|
Drop the `output/` folder contents into `@mod/addons/<mod>/config.cpp` structure and load it. Each subfolder is an independent addon with its own CfgPatches dependency.
|
||||||
|
|
||||||
## How the scripts work
|
## How the scripts work
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[mod]
|
||||||
|
mod_name = A3Rebalancer
|
||||||
|
mod_display_name = A3 Rebalancer
|
||||||
|
mod_description = Normalizes non-RHS ammo and armor values to RHS baseline.
|
||||||
|
mod_author = REDCODE
|
||||||
|
mod_version = 1.0
|
||||||
|
|
||||||
|
[paths]
|
||||||
|
; Arma 3 user mods folder (where @mod folders go)
|
||||||
|
; Windows default: Documents\Arma 3\!mods
|
||||||
|
arma_mods_dir = C:\Users\REDCODE\Documents\Arma 3\!mods
|
||||||
+85
-73
@@ -2,14 +2,14 @@
|
|||||||
generate_patches.py — Generate CfgPatches config.cpp for ammo/armor rebalance.
|
generate_patches.py — Generate CfgPatches config.cpp for ammo/armor rebalance.
|
||||||
|
|
||||||
Reads per-mod CSVs from data/, compares to RHS baseline, and generates
|
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:
|
Usage:
|
||||||
python scripts/generate_patches.py
|
python scripts/generate_patches.py
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
output/config.cpp — The rebalance mod
|
output/<mod>/config.cpp — One rebalance config per mod
|
||||||
output/unmatched.csv — Items with no RHS equivalent (for manual review)
|
output/unmatched.csv — Items with no RHS equivalent (for manual review)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
@@ -589,84 +589,75 @@ def format_number(val) -> str:
|
|||||||
return str(val)
|
return str(val)
|
||||||
|
|
||||||
|
|
||||||
def generate_config_cpp(
|
def generate_mod_config_cpp(
|
||||||
ammo_overrides: dict[str, list[AmmoOverride]],
|
mod: str,
|
||||||
armor_overrides: dict[str, list[ArmorOverride]],
|
ammo_overrides: list[AmmoOverride],
|
||||||
source_mods: set[str],
|
armor_overrides: list[ArmorOverride],
|
||||||
|
source_mods: list[str] = None,
|
||||||
) -> str:
|
) -> 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 = []
|
||||||
lines.append('// ArmADump - Rebalance Config')
|
lines.append('// ArmADump - Rebalance Config')
|
||||||
|
lines.append(f'// Mod: {mod}')
|
||||||
lines.append('// Auto-generated by generate_patches.py')
|
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('')
|
lines.append('')
|
||||||
|
|
||||||
# CfgPatches
|
# CfgPatches
|
||||||
lines.append('class CfgPatches {')
|
lines.append('class CfgPatches {')
|
||||||
for mod in sorted(source_mods):
|
lines.append(f' class armadump_{safe_name} {{')
|
||||||
safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', mod)
|
lines.append(f' units[] = {{}};')
|
||||||
lines.append(f' class armadump_{safe_name} {{')
|
lines.append(f' weapons[] = {{}};')
|
||||||
lines.append(f' units[] = {{}};')
|
lines.append(f' requiredAddons[] = {{{deps_str}}};')
|
||||||
lines.append(f' weapons[] = {{}};')
|
lines.append(f' }};')
|
||||||
lines.append(f' requiredAddons[] = {{"{mod}"}};')
|
|
||||||
lines.append(f' }};')
|
|
||||||
lines.append('};')
|
lines.append('};')
|
||||||
lines.append('')
|
lines.append('')
|
||||||
|
|
||||||
# CfgAmmo overrides
|
# CfgAmmo overrides
|
||||||
has_ammo = any(v for v in ammo_overrides.values())
|
if ammo_overrides:
|
||||||
if has_ammo:
|
|
||||||
lines.append('class CfgAmmo {')
|
lines.append('class CfgAmmo {')
|
||||||
for mod in sorted(ammo_overrides.keys()):
|
for o in sorted(ammo_overrides, key=lambda x: x.classname):
|
||||||
overrides = ammo_overrides[mod]
|
lines.append(f' class {o.classname} {{')
|
||||||
if not overrides:
|
for field_name, val in sorted(o.changes.items()):
|
||||||
continue
|
config_name = FIELD_TO_CONFIG.get(field_name, field_name)
|
||||||
for o in sorted(overrides, key=lambda x: x.classname):
|
if isinstance(val, str) and val.startswith("["):
|
||||||
lines.append(f' class {o.classname} {{')
|
sqf_val = val.replace("[", "{").replace("]", "}")
|
||||||
for field_name, val in sorted(o.changes.items()):
|
lines.append(f' {config_name}[] = {sqf_val};')
|
||||||
config_name = FIELD_TO_CONFIG.get(field_name, field_name)
|
else:
|
||||||
if isinstance(val, str) and val.startswith("["):
|
lines.append(f' {config_name} = {format_number(val)};')
|
||||||
# Array value — write as SQF array
|
lines.append(' };')
|
||||||
lines.append(f' {config_name}[] = {val};')
|
|
||||||
else:
|
|
||||||
lines.append(f' {config_name} = {format_number(val)};')
|
|
||||||
lines.append(' };')
|
|
||||||
lines.append('};')
|
lines.append('};')
|
||||||
lines.append('')
|
lines.append('')
|
||||||
|
|
||||||
# CfgWeapons overrides (armor)
|
# CfgWeapons overrides (armor)
|
||||||
has_armor = any(v for v in armor_overrides.values())
|
if armor_overrides:
|
||||||
if has_armor:
|
|
||||||
lines.append('class CfgWeapons {')
|
lines.append('class CfgWeapons {')
|
||||||
for mod in sorted(armor_overrides.keys()):
|
for o in sorted(armor_overrides, key=lambda x: x.classname):
|
||||||
overrides = armor_overrides[mod]
|
lines.append(f' class {o.classname} {{')
|
||||||
if not overrides:
|
lines.append(f' class ItemInfo {{')
|
||||||
continue
|
lines.append(f' class HitpointsProtectionInfo {{')
|
||||||
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 {{')
|
|
||||||
|
|
||||||
# Group changes by body part
|
part_changes = defaultdict(dict)
|
||||||
part_changes = defaultdict(dict)
|
for field_name, val in sorted(o.changes.items()):
|
||||||
for field_name, val in sorted(o.changes.items()):
|
section, prop = ARMOR_FIELD_MAP[field_name]
|
||||||
section, prop = ARMOR_FIELD_MAP[field_name]
|
part_changes[section][prop] = val
|
||||||
part_changes[section][prop] = val
|
|
||||||
|
|
||||||
for part in ["Head", "Neck", "Chest", "Diaphragm", "Abdomen",
|
for part in ["Head", "Neck", "Chest", "Diaphragm", "Abdomen",
|
||||||
"Body", "Arms", "Legs"]:
|
"Body", "Arms", "Legs"]:
|
||||||
if part in part_changes:
|
if part in part_changes:
|
||||||
lines.append(f' class {part} {{')
|
lines.append(f' class {part} {{')
|
||||||
for prop, val in sorted(part_changes[part].items()):
|
for prop, val in sorted(part_changes[part].items()):
|
||||||
lines.append(
|
lines.append(
|
||||||
f' {prop} = {format_number(val)};')
|
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(f' }};')
|
||||||
lines.append('};')
|
lines.append('};')
|
||||||
lines.append('')
|
lines.append('')
|
||||||
|
|
||||||
@@ -726,7 +717,7 @@ def main():
|
|||||||
print("\nScanning non-baseline mods...")
|
print("\nScanning non-baseline mods...")
|
||||||
all_ammo_overrides = {}
|
all_ammo_overrides = {}
|
||||||
all_armor_overrides = {}
|
all_armor_overrides = {}
|
||||||
all_source_mods = set()
|
all_source_mods_per_folder = defaultdict(set)
|
||||||
unmatched_ammo = []
|
unmatched_ammo = []
|
||||||
unmatched_armor = []
|
unmatched_armor = []
|
||||||
|
|
||||||
@@ -748,7 +739,8 @@ def main():
|
|||||||
ammo_ovs = find_ammo_overrides(mod_ammo, ammo_baseline)
|
ammo_ovs = find_ammo_overrides(mod_ammo, ammo_baseline)
|
||||||
if ammo_ovs:
|
if ammo_ovs:
|
||||||
all_ammo_overrides[mod_dir.name] = 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)}")
|
print(f" Ammo overrides: {len(ammo_ovs)}")
|
||||||
|
|
||||||
# Track unmatched
|
# Track unmatched
|
||||||
@@ -761,7 +753,8 @@ def main():
|
|||||||
armor_ovs = find_armor_overrides(mod_armor, armor_baseline)
|
armor_ovs = find_armor_overrides(mod_armor, armor_baseline)
|
||||||
if armor_ovs:
|
if armor_ovs:
|
||||||
all_armor_overrides[mod_dir.name] = 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)}")
|
print(f" Armor overrides: {len(armor_ovs)}")
|
||||||
|
|
||||||
for e in mod_armor:
|
for e in mod_armor:
|
||||||
@@ -770,18 +763,37 @@ def main():
|
|||||||
unmatched_armor.append(e)
|
unmatched_armor.append(e)
|
||||||
|
|
||||||
# Generate output
|
# Generate output
|
||||||
print("\nGenerating config.cpp...")
|
print("\nGenerating per-mod config files...")
|
||||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
config_cpp = generate_config_cpp(
|
# Clean old output
|
||||||
all_ammo_overrides, all_armor_overrides, all_source_mods)
|
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"
|
mods_with_overrides = set()
|
||||||
with open(config_path, "w", encoding="utf-8") as f:
|
for mod in sorted(set(list(all_ammo_overrides.keys()) + list(all_armor_overrides.keys()))):
|
||||||
f.write(config_cpp)
|
mod_ammo = all_ammo_overrides.get(mod, [])
|
||||||
print(f" Wrote {config_path}")
|
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)
|
write_unmatched(unmatched_ammo, unmatched_armor)
|
||||||
|
|
||||||
# Summary
|
# Summary
|
||||||
@@ -789,8 +801,8 @@ def main():
|
|||||||
total_armor = sum(len(v) for v in all_armor_overrides.values())
|
total_armor = sum(len(v) for v in all_armor_overrides.values())
|
||||||
print(f"\n{'=' * 60}")
|
print(f"\n{'=' * 60}")
|
||||||
print(f"DONE - {total_ammo} ammo overrides, {total_armor} armor overrides")
|
print(f"DONE - {total_ammo} ammo overrides, {total_armor} armor overrides")
|
||||||
print(f" across {len(all_source_mods)} mods")
|
print(f" across {len(mods_with_overrides)} mods")
|
||||||
print(f" Config: {config_path.relative_to(REPO_ROOT)}")
|
print(f" Output: {OUTPUT_DIR.relative_to(REPO_ROOT)}/<mod>/config.cpp")
|
||||||
print(f" Unmatched: {REPO_ROOT / 'output' / 'unmatched.csv'}")
|
print(f" Unmatched: {REPO_ROOT / 'output' / 'unmatched.csv'}")
|
||||||
print(f"{'=' * 60}")
|
print(f"{'=' * 60}")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
"""
|
||||||
|
package_mod.py — Package output/ into a loadable Arma 3 mod.
|
||||||
|
|
||||||
|
Uses armake2 to pack each addon into a .pbo file.
|
||||||
|
Creates mod.cpp and installs to your Arma 3 addons directory.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/package_mod.py
|
||||||
|
python scripts/package_mod.py --name "My Rebalance" --output "C:/Arma 3/!mods"
|
||||||
|
python scripts/package_mod.py --dry-run
|
||||||
|
|
||||||
|
Settings are read from package_mod.ini in the repo root if it exists.
|
||||||
|
Command-line arguments override INI values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
OUTPUT_DIR = REPO_ROOT / "output"
|
||||||
|
INI_PATH = REPO_ROOT / "package_mod.ini"
|
||||||
|
|
||||||
|
DEFAULTS = {
|
||||||
|
"mod_name": "A3Rebalancer",
|
||||||
|
"mod_display_name": "A3 Rebalancer",
|
||||||
|
"mod_description": "Normalizes non-RHS ammo and armor values to RHS baseline.",
|
||||||
|
"mod_author": "REDCODE",
|
||||||
|
"mod_version": "1.0",
|
||||||
|
"arma_mods_dir": str(Path.home() / "Documents" / "Arma 3" / "!mods"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def read_ini() -> dict:
|
||||||
|
"""Read package_mod.ini if it exists."""
|
||||||
|
if not INI_PATH.exists():
|
||||||
|
return {}
|
||||||
|
config = {}
|
||||||
|
current_section = None
|
||||||
|
with open(INI_PATH, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or line.startswith(";"):
|
||||||
|
continue
|
||||||
|
if line.startswith("[") and line.endswith("]"):
|
||||||
|
current_section = line[1:-1]
|
||||||
|
continue
|
||||||
|
if "=" in line:
|
||||||
|
key, _, val = line.partition("=")
|
||||||
|
key = key.strip()
|
||||||
|
val = val.strip().strip('"').strip("'")
|
||||||
|
if current_section:
|
||||||
|
key = f"{current_section}.{key}"
|
||||||
|
config[key] = val
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def generate_mod_cpp(display_name: str, description: str, author: str,
|
||||||
|
version: str) -> str:
|
||||||
|
"""Generate mod.cpp content."""
|
||||||
|
desc_escaped = description.replace('"', '""')
|
||||||
|
return f'''beta = 0;
|
||||||
|
action = "";
|
||||||
|
hideName = 0;
|
||||||
|
hidePicture = 0;
|
||||||
|
name = "{display_name}";
|
||||||
|
picture = "";
|
||||||
|
logo = "";
|
||||||
|
logoOver = "";
|
||||||
|
tooltip = "{desc_escaped}";
|
||||||
|
overview = "{desc_escaped}";
|
||||||
|
author = "{author}";
|
||||||
|
authorID = "";
|
||||||
|
version = "{version}";
|
||||||
|
versionArma[] = {{3, 0, 0}};
|
||||||
|
versionAI[] = {{3, 0, 0}};
|
||||||
|
versionExt[] = {{1, 0, 0}};
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def find_armake2() -> str:
|
||||||
|
"""Find armake2 binary."""
|
||||||
|
# Check PATH first
|
||||||
|
result = subprocess.run(
|
||||||
|
["where", "armake2"], capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return result.stdout.strip().splitlines()[0]
|
||||||
|
|
||||||
|
# Check tools/ directory
|
||||||
|
tools_path = REPO_ROOT / "tools" / "armake2.exe"
|
||||||
|
if tools_path.exists():
|
||||||
|
return str(tools_path)
|
||||||
|
|
||||||
|
print("ERROR: armake2 not found.")
|
||||||
|
print("Install from: https://github.com/KoffeinFlummi/armake2")
|
||||||
|
print("Or place armake2.exe in tools/")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def pack_pbo(armake2: str, source_dir: Path, target_pbo: Path,
|
||||||
|
prefix: str) -> bool:
|
||||||
|
"""Pack a directory into a .pbo using armake2 build (with rapification)."""
|
||||||
|
cmd = [
|
||||||
|
armake2, "build", "-f",
|
||||||
|
"-e", f"prefix={prefix}",
|
||||||
|
str(source_dir),
|
||||||
|
str(target_pbo),
|
||||||
|
]
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f" ERROR packing {source_dir.name}: {result.stderr.strip()}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Package ArmADump output into an Arma 3 mod")
|
||||||
|
parser.add_argument("--name", help="Mod folder name (default: A3Rebalancer)")
|
||||||
|
parser.add_argument("--display-name", help="Mod display name")
|
||||||
|
parser.add_argument("--description", help="Mod description")
|
||||||
|
parser.add_argument("--author", help="Mod author")
|
||||||
|
parser.add_argument("--version", help="Mod version")
|
||||||
|
parser.add_argument("--output", help="Arma 3 mods directory")
|
||||||
|
parser.add_argument("--dry-run", action="store_true",
|
||||||
|
help="Show what would be done without building")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Read INI config
|
||||||
|
ini = read_ini()
|
||||||
|
|
||||||
|
# Resolve values: CLI > INI > defaults
|
||||||
|
mod_name = args.name or ini.get("mod_name", DEFAULTS["mod_name"])
|
||||||
|
display_name = (args.display_name or
|
||||||
|
ini.get("mod_display_name", DEFAULTS["mod_display_name"]))
|
||||||
|
description = (args.description or
|
||||||
|
ini.get("mod_description", DEFAULTS["mod_description"]))
|
||||||
|
author = args.author or ini.get("mod_author", DEFAULTS["mod_author"])
|
||||||
|
version = args.version or ini.get("mod_version", DEFAULTS["mod_version"])
|
||||||
|
mods_dir = args.output or ini.get("arma_mods_dir", DEFAULTS["arma_mods_dir"])
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("ArmADump - Package Mod")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" Mod name: @{mod_name}")
|
||||||
|
print(f" Display name: {display_name}")
|
||||||
|
print(f" Author: {author}")
|
||||||
|
print(f" Version: {version}")
|
||||||
|
print(f" Output: {Path(mods_dir) / ('@' + mod_name)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Validate output dir exists
|
||||||
|
if not OUTPUT_DIR.exists():
|
||||||
|
print(f"ERROR: {OUTPUT_DIR} does not exist.")
|
||||||
|
print("Run generate_patches.py first.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Check for config.cpp files
|
||||||
|
mod_dirs = [d for d in OUTPUT_DIR.iterdir()
|
||||||
|
if d.is_dir() and (d / "config.cpp").exists()]
|
||||||
|
if not mod_dirs:
|
||||||
|
print(f"ERROR: No mod folders with config.cpp found in {OUTPUT_DIR}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f" Found {len(mod_dirs)} addon folders")
|
||||||
|
|
||||||
|
# Find armake2
|
||||||
|
if not args.dry_run:
|
||||||
|
armake2 = find_armake2()
|
||||||
|
print(f" armake2: {armake2}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Build target path
|
||||||
|
target_dir = Path(mods_dir) / f"@{mod_name}"
|
||||||
|
addons_dir = target_dir / "addons"
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print(f" [DRY RUN] Would create:")
|
||||||
|
print(f" {target_dir / 'mod.cpp'}")
|
||||||
|
for d in sorted(mod_dirs):
|
||||||
|
pbo_name = f"armadump_{d.name}.pbo"
|
||||||
|
print(f" {addons_dir / pbo_name}")
|
||||||
|
print(f"\n Total: {len(mod_dirs)} PBOs")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
addons_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Generate mod.cpp
|
||||||
|
mod_cpp = generate_mod_cpp(display_name, description, author, version)
|
||||||
|
mod_cpp_path = target_dir / "mod.cpp"
|
||||||
|
with open(mod_cpp_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(mod_cpp)
|
||||||
|
print(f" Created {mod_cpp_path}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Pack each addon into a PBO (all to temp first, then copy to final location)
|
||||||
|
prefix_base = mod_name.lower()
|
||||||
|
packed = 0
|
||||||
|
failed = 0
|
||||||
|
temp_dir = OUTPUT_DIR / "_pack_temp"
|
||||||
|
addons_temp = temp_dir / "addons"
|
||||||
|
addons_temp.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for d in sorted(mod_dirs):
|
||||||
|
pbo_name = f"armadump_{d.name}.pbo"
|
||||||
|
temp_pbo = addons_temp / pbo_name
|
||||||
|
prefix = f"{prefix_base}\\{d.name}"
|
||||||
|
|
||||||
|
print(f" Packing {d.name}...")
|
||||||
|
if pack_pbo(armake2, d, temp_pbo, prefix):
|
||||||
|
size_kb = temp_pbo.stat().st_size / 1024
|
||||||
|
print(f" -> {pbo_name} ({size_kb:.1f} KB)")
|
||||||
|
packed += 1
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
if packed == 0:
|
||||||
|
print("\nERROR: No PBOs packed.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Copy mod.cpp to temp
|
||||||
|
mod_cpp_temp = temp_dir / "mod.cpp"
|
||||||
|
shutil.copy2(str(mod_cpp_path), str(mod_cpp_temp))
|
||||||
|
|
||||||
|
# Swap: delete old dir, move temp into place
|
||||||
|
print(f"\n Installing to {target_dir}...")
|
||||||
|
try:
|
||||||
|
if target_dir.exists():
|
||||||
|
shutil.rmtree(str(target_dir))
|
||||||
|
except PermissionError:
|
||||||
|
print(" WARNING: Could not remove old directory (Arma may have it locked).")
|
||||||
|
print(" PBOs are ready in output/_pack_temp/ — close Arma and re-run, or copy manually.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.move(str(temp_dir), str(target_dir))
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Could not move to {target_dir}: {e}")
|
||||||
|
print(f" PBOs are ready at: {temp_dir}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"Done! {packed} PBOs packed, {failed} failed")
|
||||||
|
print(f" Mod: {target_dir}")
|
||||||
|
print(f" Load in Arma 3 launcher or via -mod=@{mod_name}")
|
||||||
|
print(f"{'=' * 60}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user