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:
@@ -0,0 +1,351 @@
|
||||
# Generate Patches — How It Works
|
||||
|
||||
This document explains the logic behind `generate_patches.py`, the script that produces the rebalance `config.cpp`.
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Many Arma 3 mods define ammo and armor with values that don't match RHS standards. This script:
|
||||
|
||||
1. Reads the extracted CSV data for every installed mod
|
||||
2. Compares each mod's values against RHS as the baseline
|
||||
3. Generates a `config.cpp` mod that overrides non-RHS values to match RHS equivalents
|
||||
|
||||
The result is a single drop-in mod that normalizes your entire mod list to RHS balance.
|
||||
|
||||
---
|
||||
|
||||
## Overview of the Process
|
||||
|
||||
```
|
||||
Per-mod CSV files (data/)
|
||||
|
|
||||
v
|
||||
+-------------------+
|
||||
| Load baseline | Reads RHS + vanilla + RHS sub-mods
|
||||
| (RHS / vanilla) | as the "correct" reference values
|
||||
+-------------------+
|
||||
|
|
||||
v
|
||||
+-------------------+
|
||||
| Classify every | Each ammo/armor item is sorted into a
|
||||
| entry into tiers | "tier" group based on caliber or protection level
|
||||
+-------------------+
|
||||
|
|
||||
v
|
||||
+-------------------+
|
||||
| Build baseline | For each tier, pick one representative RHS entry
|
||||
| lookup table | to use as the reference values
|
||||
+-------------------+
|
||||
|
|
||||
v
|
||||
+-------------------+
|
||||
| Compare each mod | For every non-RHS entry, look up its tier's
|
||||
| against baseline | RHS baseline and record any differences
|
||||
+-------------------+
|
||||
|
|
||||
v
|
||||
+-------------------+
|
||||
| Generate | Write a config.cpp that overrides every
|
||||
| config.cpp | differing value to match the RHS baseline
|
||||
+-------------------+
|
||||
|
|
||||
v
|
||||
output/config.cpp (the rebalance mod)
|
||||
output/unmatched.csv (items with no RHS equivalent)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Loading Baseline Data
|
||||
|
||||
The script loads CSVs from these folders as the baseline (the "correct" values):
|
||||
|
||||
| Folder | Source |
|
||||
|---|---|
|
||||
| `_rhs` | RHS AFRF (Russian equipment) |
|
||||
| `_rhsusf` | RHS USMC (American equipment) |
|
||||
| `_rhsgref` | RHS GREF (generic/other factions) |
|
||||
| `_rhssaf` | RHS SAF (Serbian equipment) |
|
||||
| `_vanilla` | Arma 3 base game (BI) |
|
||||
| `_ace` | ACE3 mod (ballistic overrides) |
|
||||
|
||||
All entries from these mods are pooled together. When two entries exist in the same tier, RHS entries are preferred over vanilla.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Classification — Putting Items Into Tiers
|
||||
|
||||
Every ammo and armor item is classified into a **tier**. Items in the same tier are considered equivalent — a 5.56mm round from mod A should have similar stats to a 5.56mm round from RHS.
|
||||
|
||||
### Ammo Classification
|
||||
|
||||
Ammo is classified by **two properties**: a **caliber tier** (the type of round) and a **subtype** (ball, AP, tracer, etc.).
|
||||
|
||||
#### How Caliber Tier Is Determined
|
||||
|
||||
The script checks the ammo class name against a list of patterns. The first pattern that matches wins. This is done by name, not by the numeric `caliber` value, because Arma's `caliber` property is a penetration coefficient, not the actual bullet caliber.
|
||||
|
||||
| Pattern in class name | Tier | Typical RHS values (hit / caliber / speed) |
|
||||
|---|---|---|
|
||||
| `9x19`, `9mm`, `9x18` | `pistol_9mm` | 5 / 0.4 / 370 |
|
||||
| `45acp`, `.45` | `pistol_45acp` | 6 / 0.5 / 260 |
|
||||
| `57x28`, `5.7` | `smg_57x28` | 4 / 0.3 / 390 |
|
||||
| `46x30`, `4.6` | `smg_46x30` | 4 / 0.3 / 375 |
|
||||
| `545x39`, `5.45` | `rifle_545x39` | 9 / 0.65 / 900 |
|
||||
| `556x45`, `5.56` | `rifle_556x45` | 9 / 0.87 / 920 |
|
||||
| `762x39` | `rifle_762x39` | 11 / 1.2 / 730 |
|
||||
| `762x51`, `.308` | `rifle_762x51` | 11.6 / 1.6 / 800 |
|
||||
| `762x54` | `rifle_762x54` | 11.6 / 1.8 / 830 |
|
||||
| `300blk` | `rifle_300blk` | (no RHS baseline) |
|
||||
| `338`, `338lapua` | `sniper_338` | (no RHS baseline) |
|
||||
| `408`, `cheytac` | `sniper_408` | (no RHS baseline) |
|
||||
| `127x99`, `50bmg`, `12.7` | `hmg_50` | 22 / 2.8 / 900 |
|
||||
| `145x114`, `14.5` | `hmg_145` | 28 / 3.5 / 950 |
|
||||
| `20mm` | `cannon_20` | 30 / 1.2 / 1000 |
|
||||
| `23mm` | `cannon_23` | 40 / 1.5 / 950 |
|
||||
| `25mm` | `cannon_25` | 60 / 2.0 / 900 |
|
||||
| `30mm` | `cannon_30` | 80 / 2.5 / 850 |
|
||||
| `35mm` | `cannon_35` | 85 / 2.5 / 850 |
|
||||
| `40mm`, `mk19` | `cannon_40` | 100 / 2.5 / 800 |
|
||||
| `mortar`, `82mm`, `81mm` | `mortar_82` | varies |
|
||||
| `100mm` and above | `gun_heavy` | varies |
|
||||
| `12gauge`, `buckshot` | `shotgun_12g` | varies |
|
||||
| `rocket`, `pg7`, `rpg` | `rocket` | varies |
|
||||
| `missile`, `titan`, `hellfire` | `missile` | varies |
|
||||
| `grenade`, `40mm_he` | `grenade_40mm` | varies |
|
||||
| `mine`, `ied`, `satchel` | `explosive` | varies |
|
||||
| `penetrator`, `heat` | `penetrator` | varies |
|
||||
|
||||
If no name pattern matches, the script falls back to the `simulation` property:
|
||||
|
||||
| Simulation | Tier |
|
||||
|---|---|
|
||||
| `shotBullet` (speed < 350) | `bullet_subsonic` |
|
||||
| `shotBullet` (other) | `bullet_unknown` |
|
||||
| `shotShell` + explosive > 0.5 | `shell_he` |
|
||||
| `shotShell` (other) | `shell_ap` |
|
||||
| `shotMissile` | `missile` |
|
||||
| `shotRocket` | `rocket` |
|
||||
| `shotGrenade` | `grenade_40mm` |
|
||||
| `shotIlluminating`, `shotSmokeX` | `utility` |
|
||||
|
||||
#### How Subtype Is Determined
|
||||
|
||||
Within each caliber tier, items are further sorted into subtypes by checking the class name for keywords:
|
||||
|
||||
| Condition | Subtype |
|
||||
|---|---|
|
||||
| Speed > 0 and < 350 m/s | `subsonic` |
|
||||
| Name contains "ap" | `ap` |
|
||||
| Name contains "tracer" | `ball_tracer` |
|
||||
| Name contains "match", "sniper", "otm", "mk262", "mk316" | `match` |
|
||||
| Name contains "incendiary" or "incen" | `incendiary` |
|
||||
| Name contains "hedp" | `hedp` |
|
||||
| Name contains "he" (but not "hedp") | `he` |
|
||||
| Everything else | `ball` |
|
||||
|
||||
### Armor Classification
|
||||
|
||||
Armor is classified by **item type** (Vest or HeadGear) and a **protection tier**.
|
||||
|
||||
#### Vest Tiers
|
||||
|
||||
Based on the `armor_chest` value (front plate protection):
|
||||
|
||||
| Chest Armor Value | Tier | Examples |
|
||||
|---|---|---|
|
||||
| 0 (no plate) | `soft` | Cloth vests, chest rigs |
|
||||
| 1 - 16 | `light` | Plate carrier lite, PASGT vest |
|
||||
| 17 - 31 | `medium` | IOTV, SPC, 6B13 |
|
||||
| 32 - 50 | `heavy` | Full battle carrier |
|
||||
| 50+ | `eod` | EOD suits, GL carriers |
|
||||
|
||||
#### Helmet Tiers
|
||||
|
||||
Based on the `armor_head` value:
|
||||
|
||||
| Head Armor Value | Tier | Examples |
|
||||
|---|---|---|
|
||||
| 0 (no armor) | `cap` | Berets, caps |
|
||||
| 1 - 6 | `light` | Cloth helmets, bump helmets |
|
||||
| 7 - 15 | `medium` | PASGT, MICH |
|
||||
| 16 - 25 | `heavy` | ACH, ECH, 6B47 |
|
||||
| 25+ | `special` | Specialized heavy helmets |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Building the Baseline Lookup Table
|
||||
|
||||
After all entries are loaded and classified, the script builds a lookup table. For each tier, it picks one representative RHS entry to use as the reference.
|
||||
|
||||
**Example:** For the `rifle_556x45` / `ball` tier, the baseline might be RHS's `B_556x45_Ball` with:
|
||||
- hit = 9
|
||||
- caliber = 0.87
|
||||
- typicalSpeed = 920
|
||||
- airFriction = -0.001033
|
||||
- deflecting = 21
|
||||
|
||||
Any 5.56mm ball round from any other mod will be compared against these values.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Finding Overrides
|
||||
|
||||
For each non-RHS mod entry, the script:
|
||||
|
||||
1. Looks up the entry's tier in the baseline table
|
||||
2. If the tier exists in the baseline, compares every field
|
||||
3. If a field differs and the mod's value is not zero, records a change to the baseline value
|
||||
|
||||
### What Gets Compared
|
||||
|
||||
**Ammo fields (14 values):**
|
||||
|
||||
| Field | Arma Config Name | What It Controls |
|
||||
|---|---|---|
|
||||
| `hit` | `hit` | Direct damage on impact |
|
||||
| `indirect_hit` | `indirectHit` | Splash damage |
|
||||
| `indirect_hit_range` | `indirectHitRange` | Splash radius (meters) |
|
||||
| `caliber` | `caliber` | Penetration coefficient |
|
||||
| `typical_speed` | `typicalSpeed` | Muzzle velocity (m/s) |
|
||||
| `air_friction` | `airFriction` | Drag coefficient |
|
||||
| `deflecting` | `deflecting` | Min ricochet angle (degrees) |
|
||||
| `explosive` | `explosive` | 0 = kinetic, 1 = explosive |
|
||||
| `ace_caliber` | `ACE_caliber` | Actual bullet diameter (mm) |
|
||||
| `ace_bullet_length` | `ACE_bulletLength` | Bullet length (mm) |
|
||||
| `ace_bullet_mass` | `ACE_bulletMass` | Bullet mass (grams) |
|
||||
| `ace_drag_model` | `ACE_dragModel` | Drag curve (1=G1, 7=G7, etc.) |
|
||||
| `ace_transonic` | `ACE_transonicStabilityCoef` | Transonic stability (0-1) |
|
||||
| `ace_mv_var` | `ACE_muzzleVelocityVariationSD` | Velocity spread (%) |
|
||||
|
||||
Plus 4 ACE array fields compared as text:
|
||||
- `ACE_ballisticCoefficients[]` — drag coefficient at different velocities
|
||||
- `ACE_velocityBoundaries[]` — velocity thresholds for BC changes
|
||||
- `ACE_muzzleVelocities[]` — muzzle velocity per barrel length
|
||||
- `ACE_barrelLengths[]` — barrel lengths for velocity interpolation
|
||||
|
||||
**Armor fields (16 values):**
|
||||
|
||||
For each body part (Head, Neck, Chest, Diaphragm, Abdomen, Body, Arms, Legs):
|
||||
- `armor` — additional armor points
|
||||
- `passThrough` — 0 = full block, 1 = no protection
|
||||
|
||||
### What Does NOT Get Overridden
|
||||
|
||||
- Fields where the mod's value is zero (treated as "not defined, keep it")
|
||||
- `passThrough` values where both the mod and RHS baseline are 1.0 (no change for unarmored areas)
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Generating config.cpp
|
||||
|
||||
The output is a standard Arma 3 `config.cpp` with three sections:
|
||||
|
||||
### CfgPatches (Dependency Declaration)
|
||||
|
||||
Each mod that has overrides gets a dependency entry:
|
||||
|
||||
```cpp
|
||||
class CfgPatches {
|
||||
class armadump_afou {
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
requiredAddons[] = {"afou"};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
This tells Arma to load our overrides after the original mod.
|
||||
|
||||
### CfgAmmo (Ammo Overrides)
|
||||
|
||||
```cpp
|
||||
class CfgAmmo {
|
||||
class B_afou_556x45_Ball {
|
||||
airFriction = -0.001033;
|
||||
caliber = 0.87;
|
||||
deflecting = 21;
|
||||
hit = 9;
|
||||
typicalSpeed = 920;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Only fields that differ from RHS are listed. Fields not listed keep their original mod values.
|
||||
|
||||
### CfgWeapons (Armor Overrides)
|
||||
|
||||
```cpp
|
||||
class CfgWeapons {
|
||||
class MyMod_Vest {
|
||||
class ItemInfo {
|
||||
class HitpointsProtectionInfo {
|
||||
class Chest {
|
||||
armor = 24;
|
||||
passThrough = 0.5;
|
||||
};
|
||||
class Abdomen {
|
||||
armor = 16;
|
||||
passThrough = 0.6;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Only body parts with differing armor values are listed.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Unmatched Items
|
||||
|
||||
Items that are classified into a tier with no RHS baseline equivalent are written to `output/unmatched.csv` for manual review.
|
||||
|
||||
**Common reasons an item is unmatched:**
|
||||
|
||||
| Reason | Example |
|
||||
|---|---|
|
||||
| Caliber has no RHS equivalent | `.300 Blackout`, `.57x28mm`, `.338 Lapua` |
|
||||
| Subtype has no RHS equivalent | Tracer variants when only ball exists in RHS |
|
||||
| Simulation-based fallback tier | Unknown simulation type |
|
||||
|
||||
These items are **not** included in the generated config.cpp. They retain their original mod values.
|
||||
|
||||
---
|
||||
|
||||
## What You End Up With
|
||||
|
||||
After running the script:
|
||||
|
||||
1. **`output/config.cpp`** — Drop this into an `@mod/addons/` folder and load it in Arma. It silently overrides every non-RHS ammo and armor value to match RHS balance.
|
||||
|
||||
2. **`output/unmatched.csv`** — A spreadsheet of items that couldn't be matched. Review these to decide if they need manual adjustment.
|
||||
|
||||
3. **No changes to original mods** — The script only reads data. Original mod files are untouched.
|
||||
|
||||
---
|
||||
|
||||
## Example: Tracing One Entry
|
||||
|
||||
Taking the AFou mod's 5.56mm ball round as an example:
|
||||
|
||||
1. **Classification**: Class name `B_afou_556x45_Ball` matches pattern `556x45` -> tier `rifle_556x45`. No "tracer", "ap", etc. in name -> subtype `ball`.
|
||||
|
||||
2. **Baseline lookup**: Tier `rifle_556x45` / `ball` maps to RHS's `B_556x45_Ball`:
|
||||
- hit = 9, caliber = 0.87, typicalSpeed = 920, airFriction = -0.001033, deflecting = 21
|
||||
|
||||
3. **Comparison**: AFou's values differ on airFriction, caliber, deflecting, hit, typicalSpeed.
|
||||
|
||||
5. **Override generated**: The config.cpp sets those 5 fields to RHS values. All other fields (ACE properties, cost, etc.) keep AFou's original values.
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Tier-based, not per-class matching**: All 5.56mm ball rounds are normalized to the same RHS reference. If a mod intentionally makes a round weaker or stronger (e.g., training ammo), it will be overwritten.
|
||||
- **No threshold filtering**: Even a difference of 0.001 generates an override. There is no minimum difference filter.
|
||||
- **First RHS entry wins**: If multiple RHS entries exist in the same tier, the first one encountered is used. This is usually fine since RHS values within a tier are consistent.
|
||||
- **Unmatched items skipped**: Calibers without an RHS equivalent (like .300 BLK) are left untouched.
|
||||
Reference in New Issue
Block a user