@poe2-toolkit/mod-extractor
v1.0.0
Published
Builds Path of Exile 2 item-mod data - affix ranges, tiers and spawn tags - straight from the official GGPK / patch server. Code only - ships no game data or art.
Maintainers
Readme
@poe2-toolkit/mod-extractor
Builds Path of Exile 2 item-mod data - affix roll ranges, tiers and the item-type tags that gate where each mod can roll - straight from the official GGPK / patch server, in a flat shape a build front-end can consume.
It mirrors @poe2-toolkit/item-extractor and
@poe2-toolkit/rune-extractor: source-agnostic, built on
a @poe2-toolkit/ggpk source, returning formatted data rather than
writing into the package. Each mod's spawnWeights use the same Tags.Id
vocabulary the item extractor puts on each item's tags, so mod data and item
data join on tags with no shared code - the intended use is to show, for a given
item, only the mods that can roll on it.
Code only. This package ships no game data and no art. Everything it produces is read from the patch server at run time and handed back to you.
Install
npm install @poe2-toolkit/mod-extractor @poe2-toolkit/ggpkNode 18+. ESM only. TypeScript types are included.
The contract
The library returns formatted data. It performs no I/O of its own beyond what the source serves, and it never writes to disk.
import { createCdnSource } from '@poe2-toolkit/ggpk';
import { extractMods } from '@poe2-toolkit/mod-extractor';
const source = await createCdnSource({
patch: '4.5.4.1',
tablesDir: './tables/English',
cacheDir: './.cache',
});
const { data } = await extractMods(source);
patchis whatever version the patch server currently serves; a stale version 404s, so pass the one you actually want to extract.
extractMods(source) resolves to a ModBundle. Mods carry no art, so unlike the
item, gem and rune bundles it has only data (there is no icons):
interface ModBundle {
data: ModData; // mods keyed by Mods.Id
}buildMods(source) is exported too and returns the ModData directly.
Field-level docs live on the exported types themselves - Mod, ModRoll,
ModSpawnWeight - so your editor shows each field's meaning on hover and they ship
in the .d.ts. The rest of this section is the shape and the rules the types alone
don't tell you.
data: the mods (ModData)
ModData is a plain object keyed by Mods.Id - the stable internal mod id
(e.g. LocalIncreasedPhysicalDamagePercent8), since a mod has no single display
name. Each value is a Mod:
"LocalIncreasedPhysicalDamagePercent8": {
"name": "Merciless",
"domain": "Item",
"generationType": "Prefix",
"group": "LocalPhysicalDamagePercent",
"tier": 8,
"level": 82,
"stats": ["(170-179)% increased Physical Damage"],
"rolls": [{ "stat": "local_physical_damage_+%", "min": 170, "max": 179 }],
"families": ["LocalPhysicalDamagePercent"],
"spawnWeights": [
{ "tag": "weapon", "weight": 1 },
{ "tag": "default", "weight": 0 }
]
}statsare the rendered roll, each stat line with its range shown as(min-max)(+(9-16) to Armour,Adds (1-2) to (4-5) Physical Damage). A fixed roll (min equal to max) renders as a plain number.rollscarries the same rolls structured -{ stat, min, max }per stat - so you can do your own math; a two-stat line yields two rolls.tieris the affix ladder position, 1-based by ascendinglevel: tier 1 is the weakest tier, tier 8 the strongest, matching the numeric suffix on the id. The ladder is scoped to(group, domain, generationType), so an eight-tier item prefix ranks 1..8 even though itsgroupalso holds the fixed unique rolls of the same modifier.nullwhen the mod has no group.spawnWeightsis the item-type gate - which item types the mod can appear on. A mod can roll on an item when the first of itsspawnWeightswhosetagthe item carries has aweightabove zero;weight0 blocks that tag, and the trailingdefault(which every item carries) is the catch-all. Order matters, so it is a list, not a map.domainandgenerationTypeare resolved from GGG'sModDomains/ModGenerationTypeenums. The rollable equipment affix pool isdomain: "Item"withgenerationType: "Prefix"or"Suffix"; other values cover uniques, corruptions, monsters and so on.familiesare mutual-exclusion group ids: two mods sharing a family cannot both roll on one item.
Joining mods to items
A mod and an item from @poe2-toolkit/item-extractor share
two vocabularies - the domain (mod.domain / item.modDomain) and the spawn tags
(mod.spawnWeights[].tag / item.tags) - so the compatible mods for an item are a
pure filter: no lookup tables, no shared code. A mod rolls when it is in the item's
domain and the first of its spawnWeights whose tag the item carries has
a positive weight. Filter on the domain first - many mods carry a positive default
weight, so tag-matching alone would let unrelated domains (Monster, Heist, Atlas, ...)
leak onto every item:
function compatibleMods(item: { modDomain: string | null; tags: string[] }, mods: ModData): string[] {
return Object.entries(mods)
.filter(([, mod]) => {
if (mod.domain !== item.modDomain) return false;
const gate = mod.spawnWeights.find((sw) => sw.tag === 'default' || item.tags.includes(sw.tag));
return gate != null && gate.weight > 0;
})
.map(([id]) => id);
}Flasks and charms live in domain: "Flask", ordinary equipment in domain: "Item",
so a flask never draws an equipment affix and vice versa.
CLI: write the bundle to disk
poe2-mod-extract \
--patch 4.5.4.1 \
--tables ./tables/English \
--cache ./.cache \
--out ./out/modsAll four flags are required. It writes mods.json.
How it works
- Each mod carries its roll as a numeric
(stat id, [min, max])range per stat. The stat-description engine from@poe2-toolkit/ggpk(GGG's ownstat_descriptions.csd, read straight from the GGPK) renders each stat to text; the min-value and max-value passes are then merged into one ranged line. A value-dependent variant (different text at the two ends of a range) keeps the max-value line whole rather than producing a malformed range. - Tiers are ranked within
(group, domain, generationType)by ascending level, because oneModTypegroup holds the same modifier for several contexts (its eight item prefixes sit beside dozens of fixed unique rolls); ranking the whole group would inflate the numbers. - Spawn weights are read from
SpawnWeight_Tags/SpawnWeight_Valuesin order, mapping each tag index to itsTags.Id. This is GGG's own first-match gate for where a mod can roll. - Mods carry no icons, so there is nothing to decode; the bundle is data only.
Attributions and legal
This is an unofficial, fan-made project, not affiliated with, endorsed by, or sponsored by Grinding Gear Games. "Path of Exile 2" is a trademark of Grinding Gear Games, and all game content, data, and art are their property. This package ships code only and stores nothing derived from the game. Thank you to Grinding Gear Games for making Path of Exile 2.
GGPK access is provided by @poe2-toolkit/ggpk, which builds on
pathofexile-dat (MIT, © SnosMe).
Full attribution is in the repository NOTICE.
License
MIT - see LICENSE.
