The pipeline behind e7codex.com: a static, browsable archive of Epic Seven character/artifact art with a live in-browser Spine model viewer. This repo is the tooling — it turns raw, extracted game assets into the finished static site. It ships no game assets; you bring your own.
What's here: the indexer, the asset-staging + pose-rendering pipeline, the SCSP→Spine converters (with attribution — see
CREDITS.md), and the site UI.What's NOT here (by design): any Epic Seven assets, the cached spine-player runtime, and the community name databases. The steps below fetch or generate each of those locally.
raw .scsp/.sct/.atlas ──► converters ──► staged Spine JSON ──► site/assets/<slug>/
(you supply) (this repo) + decoded .png │
▼
build_index.py ──► site/data/*.json ◄── names from
community DBs
render_poses.js ──► pose.png thumbnails
│
▼
site/index.html + viewer.html
- Python 3.11+ (
pip install lz4 pillow texture2ddecoder) - Node 18+ (
npm install— pulls puppeteer + sharp for thumbnail rendering) - Your own extracted Epic Seven assets (see step 2)
spine-player is not redistributed here. Fetch stock 3.8 from Esoteric and apply the one-line screenshot patch:
curl -sSL -A "Mozilla/5.0" -o site/spine-player.js https://esotericsoftware.com/files/spine-player/3.8/spine-player.js
curl -sSL -A "Mozilla/5.0" -o site/spine-player.css https://esotericsoftware.com/files/spine-player/3.8/spine-player.css
copy site\spine-player.js tools\spine-player.js
copy site\spine-player.css tools\spine-player.cssThen, in site/spine-player.js (around line 11071), enable buffer readback so
the viewer's screenshot button works:
// find: var webglConfig = { alpha: config.alpha };
// change: var webglConfig = { alpha: config.alpha, preserveDrawingBuffer: true };Optional — Spine-2.1.x shear-free scale inheritance. Spine 2.1.x tracked
world scale as scalars and rebuilt a clean rotation×scale matrix per bone, so a
non-uniform-scaled rotated parent never sheared its children. spine-player 3.8
composes full 2×2 matrices, which do accumulate shear there — exploding
long-aspect weapon meshes on some 2.1.27 combat rigs into needles. The converter
tags affected rigs with "e7v21x": true; a stock player ignores the flag (the
needle bug remains). To honor it, in site/spine-player.js find the start of
Bone.prototype.updateWorldTransformWith — just after var parent = this.parent;
(stock 3.8 follows it with if (parent == null) {) — and insert this gated
branch before that if:
if (this.skeleton.data.e7v21x) {
var sk = this.skeleton;
var cosD = spine.MathUtils.cosDeg, sinD = spine.MathUtils.sinDeg;
if (parent == null) {
var wr = rotation, wsx = scaleX * sk.scaleX, wsy = scaleY * sk.scaleY;
this._e7wr = wr; this._e7wsx = wsx; this._e7wsy = wsy;
this.a = cosD(wr) * wsx; this.b = -sinD(wr) * wsy;
this.c = sinD(wr) * wsx; this.d = cosD(wr) * wsy;
this.worldX = x * sk.scaleX + sk.x;
this.worldY = y * sk.scaleY + sk.y;
return;
}
var pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
this.worldX = pa * x + pb * y + parent.worldX;
this.worldY = pc * x + pd * y + parent.worldY;
var pwr = parent._e7wr || 0;
var pwsx = parent._e7wsx != null ? parent._e7wsx : 1;
var pwsy = parent._e7wsy != null ? parent._e7wsy : 1;
var tm = this.data.transformMode;
// Normal(0)/NoScale(3)/NoScaleOrReflection(4) inherit rotation;
// Normal(0)/NoRotationOrReflection(2) inherit scale.
var inhRot = (tm == 0 || tm == 3 || tm == 4);
var inhScale = (tm == 0 || tm == 2);
var wr = inhRot ? pwr + rotation : rotation;
var wsx = inhScale ? pwsx * scaleX : scaleX;
var wsy = inhScale ? pwsy * scaleY : scaleY;
this._e7wr = wr; this._e7wsx = wsx; this._e7wsy = wsy;
this.a = cosD(wr) * wsx; this.b = -sinD(wr) * wsy;
this.c = sinD(wr) * wsx; this.d = cosD(wr) * wsy;
return;
}You also need the loader to carry the flag through: find
skeletonData.imagesPath = skeletonMap.images; and add
skeletonData.e7v21x = skeletonMap.e7v21x; after it. The branch is gated on the
flag, so 3.8.99 rigs and any bone without non-uniform scale under rotation are
untouched.
This is the step you do yourself. You need:
- Raw rigs — the game's
.scspskeleton files (+.scttextures,.atlas) for portraits and combat models. Extracting these from the client is on you; the community tool for it is EpicSevenAssetRipper. - Names / slugs — point the indexer at the public community databases
(ceciliabot, epic7rtastats). See
CREDITS.md. These are not bundled.- Optional, self-sufficient: if you have your own decrypted game data +
keys set up (see the voice pipeline below — same
voice_keys.json), runpython tools/build_names.pyto pull names + rarity/attribute/role straight from the game intodata_external/names_from_db.json. The indexer layers it ahead of the community DBs. This also writesunreleased_units.json(units the game still labels "Unknown Hero"); the indexer drops those — the project does not publish unannounced/datamined units. Both files are gitignored. - Artifacts (same setup):
python tools/build_artifacts.pydecodesequip_item.dbintodata_external/artifacts_from_db.json, layered ahead of the communityArtifacts.jsonfor name/rarity/role. Gitignored. - Localization (same setup):
python tools/build_i18n.pydecodes each language'stext.dbinto per-language display-name overlays (site/data/lang/<lang>.json) that power the site's language switcher (10 game languages + unofficial Vietnamese). It needs the same keys/data asbuild_names.py, plus hand-maintained UI-chrome translations (data_external/i18n_ui/<lang>.json, not bundled). Without the overlays the switcher still renders and every name falls back to English.
- Optional, self-sufficient: if you have your own decrypted game data +
keys set up (see the voice pipeline below — same
Tell the pipeline where your data lives in one place: copy
tools/voice_keys.example.json to tools/voice_keys.json (gitignored) and set
raw_dir (your extracted output/), img_dir (decoded images), and voice_dir
(voice scratch). tools/paths.py resolves these for every tool, so there's no
code to edit; omitted keys default under dump_dir.
python tools/prepare_assets.py --all # portraits → site/assets/<slug>/
python tools/prepare_combat_assets.py --all # combat rigs (optional)tools/scsp_to_json.py auto-detects the rig version (2.1.27, 3.8.x, or 4.2.x)
and dispatches to the right converter — the vendored third-party ones for
2.1.27 / 3.8.x, and tools/skel42_to_json38.py for Spine 4.2 rigs (an
E7-wrapped stock .skel, converted down to 3.8-compatible JSON so the same
spine-player renders every rig).
node tools/render_poses.js # bakes site/assets/<slug>/pose.pngOptionally, bake tighter character-only hub thumbnails (the site falls back to
pose.png when these are absent). Smart-crop is baked into the PNG at render
time:
node tools/render_thumbs.js # site/assets/<slug>/thumb.png (FX/backdrop stripped, smart-cropped)python build_index.py --img <your_img_dir> --raw <your_raw_dir> --out ./sitecd site
python -m http.server 8765
# visit http://localhost:8765/Note:
site/index.html,viewer.html, and404.htmlreferencefavicon-16.png,favicon-32.png, andapple-touch-icon.png, which are not shipped. Drop your own icons intosite/or remove the<link rel="icon">tags — the site works either way (the browser just shows a default favicon).
The site can show per-hero voice-actor credits and a clickable catalog of voice clips. This is an optional add-on with extra prerequisites, and like the rest of the repo it ships no game data and no keys — you supply both.
You'll need:
- vgmstream — download a release build from
vgmstream and unzip it into
tools/vendor/vgmstream/(sotools/vendor/vgmstream/vgmstream-cli.exeexists). - FFmpeg — install it and make sure
ffmpegis on yourPATH. - Your own keys + data — copy
tools/voice_keys.example.jsontotools/voice_keys.json(gitignored) and fill in the values from your own install:dump_dir(+ optionalraw_dir/img_dir/voice_dir), your outer-XOR key file (outer_key_file), and the default XXTEA key (default_xxtea_key). Forsync_voice_banks.py, optionally setgame_sound_dir(your read-only<game install>/data.unpacked/sound). None of these are provided here. - Your own voice banks — the FMOD
.bankfiles, under<sound_dir>/<lang>/*.bank.
Then:
# Credits + a fallback line catalog from your local data files → site/data/voices.json
python tools/build_voices.py
# (optional) copy banks OUT of your game install into the voice scratch tree,
# with a download-settle wait + per-language freshness gate (never writes to the game folder)
python tools/sync_voice_banks.py --extract # syncs + chains extraction for changed langs
# Decode the voice banks → OGG + a per-language catalog
python tools/extract_voice_audio.py --langs en --slugs c1001 # subset / pilot
python tools/extract_voice_audio.py --langs en ja ko --all # fullextract_voice_audio.py reads banks from / writes OGGs to your configured
voice_dir (from voice_keys.json, default <dump_dir>/_voice_work); override
per-run with --sound / --out (or the E7_VOICE_SOUND / E7_VOICE_OUT env
vars). The generated audio and JSON stay local — they're gitignored and not part
of this repository.
site/ is a self-contained static bundle — serve it from any static host
(GitHub Pages, Cloudflare Pages, Netlify, a plain web server, etc.). The Spine
assets under site/assets/ are large; for production you'll likely want to
offload them to object storage and point the viewer at that host instead of
serving them inline. That wiring is left to you.
See CREDITS.md for full attribution and LICENSE for terms. In short: the
E7 Codex code is MIT; the vendored converters keep their original authors'
terms; spine-player is Esoteric Software's; and Epic Seven assets belong to
Smilegate / Super Creative and are not part of this repository.