@poe2-toolkit/ggpk
v1.0.0
Published
Access layer for Path of Exile 2's official GGPK / patch server: fetches and decodes game tables and raw files, and decodes GGPK image and stat-description formats. The shared base every PoE2 data extractor builds on.
Maintainers
Readme
@poe2-toolkit/ggpk
Shared access layer for Path of Exile 2's official GGPK / patch server. It fetches and decodes game tables and raw files, decodes the GGPK image and stat-description formats, and hands the result back to you as plain data.
This is the foundation every PoE2 data extractor in this toolkit builds on. It is the only package that talks to the network, so the extractors stay agnostic to where their bytes come from.
Code only. This package ships no game data and no art. It reads from the official patch server (or your own game files) at run time and returns the decoded result; it never bundles or redistributes anything from the game.
Install
npm install @poe2-toolkit/ggpkNode 18+. ESM only. TypeScript types are included.
The contract
Everything is built around one small interface, the boundary an extractor depends on and the only thing that knows where bytes come from:
interface GgpkSource {
/** Decoded rows of a GGPK data table, e.g. "PassiveSkills", in table order. */
table(name: string): Promise<TableRow[]>;
/** Raw bytes of a GGPK file by logical path, or null if it cannot be served. */
file(path: string): Promise<Uint8Array | null>;
}An extractor asks a GgpkSource for tables and files. Whether those come from
the patch CDN, a local game install, or a pre-extracted cache is the source's
concern, never the extractor's. The interface is dependency-free on purpose: an
extractor that imports only the type pulls in none of the acquisition stack.
The default source: the patch CDN
createCdnSource is the batteries-included GgpkSource. It serves tables from a
directory of pathofexile-dat-decoded
JSON files and pulls raw files and sprites from the patch CDN on demand, caching
them on disk.
import { createCdnSource } from '@poe2-toolkit/ggpk';
const source = await createCdnSource({
patch: '4.5.3.1.7', // GGPK patch version
tablesDir: './tables/English', // pathofexile-dat's decoded <Name>.json output
cacheDir: './.cache', // where downloaded bundles are cached
});
const characters = await source.table('Characters');
const psg = await source.file('metadata/passiveskillgraph.psg');Producing the decoded tables is a one-time step with pathofexile-dat's own CLI;
point tablesDir at its output. Connecting to the network is deferred to the
first file or sprite request, so table reads only touch local disk.
cdnHost is optional and defaults to the PoE2 patch server
(https://patch-poe2.poecdn.com); override it to point at a mirror. The bundle
cache lives in a <patch>/ subdirectory of cacheDir.
On top of GgpkSource, the CDN source adds image fetching for art-heavy
extraction:
interface GgpkImageSource {
/** Decode a DDS by its GGPK path (BC1/BC2/BC3/BC7), cached. */
dds(path: string): Promise<RgbaImage | null>;
/** Resolve a UIImages logical name to its backing DDS and rect. */
resolveSprite(name: string): Promise<SpriteRef | null>;
/** A UIImages sprite decoded and cropped to its rect. */
uiSprite(name: string): Promise<RgbaImage | null>;
}createCdnSource takes CdnSourceOptions and returns a CdnSource
(GgpkSource & GgpkImageSource); resolveSprite yields a SpriteRef (a backing
DDS path plus a sub-rect). These, along with TableRow, are exported — see the
exported types for full field docs.
Shared decoders
The package also exports the format decoders every domain reuses, so they live in one place rather than being reimplemented per extractor:
| Export | What it does |
| --- | --- |
| decodeDds(bytes) | Decode a DDS buffer (BC1/BC2/BC3/BC7) to straight RGBA8. |
| encodePng(width, height, rgba) | Encode RGBA8 to a PNG buffer (Node zlib, no native deps). |
| decodePng(bytes) | Decode an 8-bit RGBA/RGB PNG to RGBA8. |
| buildStatIndex(csd) | Parse a stat_descriptions.csd (UTF-16 text) into a per-stat index. |
| renderBlock(index, statIds, vals) | Render numeric (stat, value) pairs into human-readable lines. |
| decodeDdsIcons(source, ddsPaths, transform?, concurrency?) | Decode a set of distinct DDS paths to PNG, skipping and reporting what the source can't serve. |
| mapConcurrent(items, concurrency, fn) | Run fn over items with at most concurrency calls in flight, preserving output order. |
buildStatIndex returns a StatIndex; pass it to renderBlock along with
parallel statIds/vals arrays. renderBlock returns a RenderedBlock with
lines (the rendered text) and unresolved (any stat ids with no matching
block). RgbaImage ({ width, height, rgba }) is the shape returned by every
image decoder. All of these types are exported; see the exported types for full
field docs.
decodeDdsIcons is the icon-decode loop every icon-shipping extractor (item,
gem, rune) runs: given a DdsSource (anything with a dds(path) method) and an
iterable of DDS paths, it decodes each distinct path to PNG and returns a
DdsIconsResult ({ icons, report: { packed, missing } }) - no vendored
fallback, a path the source can't serve or decode is just skipped and counted.
The optional transform(img, ddsPath) hook runs on the decoded image before
encoding, for extractor-specific post-processing (e.g. item-extractor's
flask-sheet compositing). Up to concurrency distinct paths (default 16)
decode in flight at once - each decode is dominated by an awaited network
fetch, not CPU work, so overlapping them cuts wall-clock time without
changing the result. mapConcurrent is the general-purpose version of that
same worker-pool loop, for any other independent-awaits-in-a-loop case (e.g.
the tree extractor's sprite atlas build); it guarantees results land at their
original index, so callers that depend on input order (like atlas packing)
aren't affected by which call happens to finish first.
Concurrent requests for a bundle the CDN cache hasn't fetched yet (e.g. two
decodeDdsIcons workers whose DDS paths happen to live in the same bundle)
share a single in-flight fetch instead of each downloading the same bytes -
handled internally by createCdnSource, nothing callers need to do. Cache
writes are published atomically (temp file + rename), so a cache directory is
safe to share across worker threads and processes: a concurrent reader either
sees a complete bundle or no file, never a partial write.
All of it is pure TypeScript with no native dependencies, which keeps extraction portable across machines and CI.
CLI building blocks (@poe2-toolkit/ggpk/cli)
A separate subpath export for extractor CLIs, kept out of the main entry since it's a CLI-authoring concern rather than part of the GGPK access layer:
| Export | What it does |
| --- | --- |
| parseExtractorArgs(argv, usage) | Parse --patch/--tables/--cache/--out; throws with usage on a missing flag. |
| writeIconTree(iconsDir, icons) | Write a { path: png } icon map under iconsDir, creating subdirectories as needed. |
| runCli(main) | Run a CLI main, printing its error to stderr and setting exit code 1 on failure. |
Every extractor's CLI is a few lines of package-specific output on top of these
three: parse the flags, createCdnSource, run the extractor, write its output,
print a summary.
A note on pathofexile-dat
The bundle loader and sprite-layout parser this package relies on live in
pathofexile-dat's internal dist/ paths. The exact internal layout can change
between major versions, so pathofexile-dat is pinned as a dependency. See
NOTICE for attribution.
License
MIT. See LICENSE. Not affiliated with Grinding Gear Games; see NOTICE.
