Def Files
The stats file sends weapons, ammo, and vehicles as ids, and each game and mod names its ids in its own def files. The below example code will read a pff archive and extract the weapon, ammo, and item definitions for display. You can also pass a def file directly to the script.
For building your own stats page, you should extract the def files listed below along with the game's gametext.bin for object names and save them in a location accessible to your script.
NovaHQ has a tool you can use to extract the necessary files from the game's pff archives: NovaPff.
File List
| File | Names | Id |
|---|---|---|
weapon.def |
Weapons | The weapon's place in the file, counted from 1 |
ammo.def |
Ammo | The ammo's place in the file, counted from 0 |
items.def |
Vehicles and objects | The item's id less 100000 |
gametext.bin |
Weapon display names | Keyed by each weapon's identifier in weapon.def |
Example Def Parser (PHP 8+)
<?php declare(strict_types=1);
// Retrieve the weapon, ammo, and vehicle names of a game or mod, or of
// a single def file, as JSON keyed by the ids the stats file provides.
//
// php definitions.php "C:\Games\Joint Operations"
// php definitions.php "C:\Games\Joint Operations" "C:\Games\Joint Operations\expansion\jox01"
// php definitions.php "C:\Mods\weapon.def" "C:\Mods\gametext.bin"
// Game archives in the order they are searched for files
const GAME_PFFS = ['language.pff', 'localres.pff', 'resource.pff'];
// The secret key used to decrypt SCR def files
const SCR_KEY = 0x2A5A8EAD;
// An items.def id less this base is the id the stats file provides
const ITEM_ID_BASE = 100000;
// The JSON output options
const JSON_FLAGS = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT;
// Read a file from the first archive that contains it. The
// header and entry table hold 32-bit little-endian ints.
function pffRead(array $pffs, string $name): ?string {
foreach($pffs as $pff) {
if(!is_file($pff))
continue;
$file = fopen($pff, 'rb');
['count' => $count, 'size' => $size, 'table' => $table] = unpack('x8/Vcount/Vsize/Vtable', fread($file, 20));
fseek($file, $table);
$entries = fread($file, $count * $size);
for($i = 0; $i < $count; $i++) {
$entry = unpack('Vdeleted/Voffset/Vlength/x4/Z16name', $entries, $i * $size);
if($entry['deleted'] === 0 && strcasecmp($entry['name'], $name) === 0) {
fseek($file, $entry['offset']);
return fread($file, $entry['length']);
}
}
}
return null;
}
// Decrypt and read a def file as text. If the file does not start
// with the SCR header it is treated as plain text.
function defText(string $data): string {
if(str_starts_with($data, "SCR\x01")) {
$data = strrev(substr($data, 4));
$key = SCR_KEY;
for($i = 0, $size = strlen($data); $i < $size; $i++) {
$key = rol(($key + rol($key, 11)) & 0xFFFFFFFF, 4) ^ 1;
$data[$i] = chr(ord($data[$i]) ^ ($key & 0xFF));
}
$data = rtrim($data, "\0");
}
$text = mb_convert_encoding($data, 'UTF-8', 'Windows-1252');
return (string)preg_replace('~("[^"\r\n]*")|//[^\r\n]*~', '$1', $text);
}
// Rotate a 32-bit value left
function rol(int $value, int $bits): int {
return (($value << $bits) | ($value >> (32 - $bits))) & 0xFFFFFFFF;
}
// Map each gametext.bin identifier to its text. The group
// counts and entry offsets are 32-bit little-endian ints.
function gameText(string $data): array {
['names' => $names, 'count' => $count] = unpack('x4/Vnames/x4/Vcount', $data);
$base = 16 + 16 * $count;
$texts = [];
$index = 0;
for($g = 0, $groups = unpack('V', $data, $names)[1]; $g < $groups; $g++) {
['ids' => $ids, 'count' => $size] = unpack('Vids/Vcount', $data, $names + 4 + 8 * $g);
$ids += $names + 4;
for($e = 0; $e < $size; $e++, $index++) {
$id = cstr($data, $ids);
$offset = $base + unpack('V', $data, 16 + 16 * $index)[1];
$texts[strtolower($id)] ??= mb_convert_encoding(cstr($data, $offset), 'UTF-8', 'Windows-1252');
}
}
return $texts;
}
// Read a zero-ended string and move the offset past it
function cstr(string $data, int &$offset): string {
$end = strpos($data, "\0", $offset);
$text = substr($data, $offset, $end - $offset);
$offset = $end + 1;
return $text;
}
// Resolve each weapon's name from weapon.def, counted from 1, using the
// gametext of its identifier or loadout_menu_textid, else the identifier.
function weaponNames(string $text, array $gameText): array {
preg_match_all('/^\s*weapon\s+"([^"]*)"/m', $text, $matches, PREG_OFFSET_CAPTURE);
$names = [];
foreach($matches[1] as $i => [$id, $start]) {
$block = substr($text, $start, ($matches[0][$i + 1][1] ?? strlen($text)) - $start);
$name = $gameText[strtolower($id)] ?? '';
if(trim($name) === '' && preg_match('/^\s*loadout_menu_textid\s+(\S+)/mi', $block, $label))
$name = $gameText[strtolower($label[1])] ?? '';
$names[$i + 1] = trim($name) !== '' ? $name : $id;
}
return $names;
}
// Resolve each ammo's name from ammo.def, counted from 0
function ammoNames(string $text): array {
preg_match_all('/^\s*ammo\s+(\S+)/m', $text, $matches);
return $matches[1];
}
// Resolve each vehicle's and object's name from items.def by its id less
// ITEM_ID_BASE. Only use items with an hp line, repeated items are skipped.
function itemNames(string $text): array {
$names = [];
foreach(preg_split('/^\s*begin\s+/mi', $text) as $block) {
if(preg_match('/^"([^"]*)"/', $block, $name)
&& preg_match('/^\s*id\s+(\d+)/mi', $block, $id)
&& preg_match('/^\s*hp\s/mi', $block))
$names[(int)$id[1] - ITEM_ID_BASE] ??= $name[1];
}
ksort($names);
return $names;
}
// Resolve the names in a single def file by its file name. A weapon.def
// uses the gametext.bin passed after it for display names.
function defNames(string $file, ?string $gametext): array {
$text = defText(file_get_contents($file));
return match(strtolower(basename($file))) {
'weapon.def' => weaponNames($text, $gametext !== null ? gameText(file_get_contents($gametext)) : []),
'ammo.def' => ammoNames($text),
'items.def' => itemNames($text),
default => exit("Not a weapon.def, ammo.def, or items.def\n"),
};
}
if(!isset($argv[1]))
exit("No game folder or def file specified\n");
// A def file passed directly prints its own names
if(is_file($argv[1]))
exit(json_encode(defNames($argv[1], $argv[2] ?? null), JSON_FLAGS) . "\n");
// Load a mod's archives before the game's so its files override the game's
$pffs = array_map(fn(string $pff): string => "{$argv[1]}/{$pff}", GAME_PFFS);
if(isset($argv[2])) {
$mod = basename($argv[2]);
array_unshift($pffs, "{$argv[2]}/{$mod}L.pff", "{$argv[2]}/{$mod}.pff");
}
$files = [];
foreach(['weapon.def', 'gametext.bin', 'ammo.def', 'items.def'] as $name)
$files[$name] = pffRead($pffs, $name) ?? exit("No archive holds {$name}\n");
echo json_encode([
'weapons' => weaponNames(defText($files['weapon.def']), gameText($files['gametext.bin'])),
'ammo' => ammoNames(defText($files['ammo.def'])),
'vehicles' => itemNames(defText($files['items.def'])),
], JSON_FLAGS), "\n";