Files
A3Rebalancer/GENERATE_PATCHES_EXPLAINED.md
T
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

352 lines
12 KiB
Markdown

# 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.