Team Cereustooling wiki

Port a Dark Souls II character

Partly checkedagainst the code on 2026-11-12 11:37

This page covers the build route for a DS2 (Scholar of the First Sin) humanoid as a Nightreign chr. One Kotlin script does the whole port. A small Python script per port wraps it. The C# tools and the DS2 Python steps come from eclipse-build.

Checked against code: the wrapper scripts, npmirror.py, anim_stage.py, the tool entry points, and stages 1 to 5 and 7 to 10 of the Kotlin script. Not checked: the per-port data file formats (port.json, slots.json, animmap.csv, tae_extra.json), the _ds2audit kit, and anything in game.

EB means py -3.12 -I -B S:/eclipse-build/eb.py. Shared tools only: see ../build/use-shared-tools.md.

What you need

  • DS2 SotFS and Nightreign installed. The script's defaults are S:/SteamLibrary/steamapps/common/Dark Souls II Scholar of the First Sin/Game and S:/SteamLibrary/steamapps/common/ELDEN RING NIGHTREIGN/Game (--ds2-game, --nr-game).
  • The pinned Python at S:/toolchains/python-ds2/Scripts/python.exe. If it is missing, build it: py -3.12 -I -B S:/eclipse-build/toolchain/python.py ds2. The Kotlin script uses it when it exists, else python from PATH.
  • The installed script host: S:/souls-script-kt/script-host/build/install/script-host/bin/script-host.bat.
  • A port folder under S:/souls-script-kt/scripts/tools/npc-ports/ds2-<name>/ with data/<slug>_port.json, the other data/<slug>_* files and ai/<goal>_battle.lua. Copy an existing port (for example ds2-heide-knight) as the pattern.
  • About 4 GB of memory for one port build.

Steps

  1. Find out how DS2 really plays each clip before you fill Nightreign animation slots. See Read DS2's clip usage below.

  2. Write the port's data files and its build.py. The existing build.py files all have the same shape (next section).

  3. Run the build twice, into two work folders, as an eb job. Example with the Heide Knight wrapper:

    EB run --mem 4 -- py -3.12 -I -B S:/souls-script-kt/scripts/tools/npc-ports/ds2-heide-knight/build.py sword --work <work>/run-A --stage <stage>/run-A
    EB run --mem 4 -- py -3.12 -I -B S:/souls-script-kt/scripts/tools/npc-ports/ds2-heide-knight/build.py sword --work <work>/run-B --stage <stage>/run-B
    

    --stage DIR copies the shipped files to DIR/mod and writes DIR/<chr>.sha256. --skip-kts reuses the Kotlin script's output and only redoes the later steps. ds2build.py also has --no-anim-ops.

  4. Read output/npc-port-<slug>/reports/port-lint-final.md. Fix every FAIL.

  5. Make the stage with EB stage make (see ../build/start-a-stage.md).

What a build.py does

Read from ds2-belfry-gargoyle/ds2build.py and ds2-heide-knight/build.py:

  1. Sets a private APPDATA per build. Soulstruct writes a fixed %APPDATA%\__temp_spline__.hkx, so two builds sharing it break each other.

  2. Calls npmirror.tools(work, NP, run) from S:/eclipse-build/tools/ds2/npmirror.py. This makes <work>/np, a mirror of npc-ports built from junctions, in which each tool's bin folder is the eclipse-build build of that tool.

  3. Waits for free commit memory (6 GB in ds2build.py, 8 GB in the Heide Knight script), then runs the Kotlin script:

    script-host.bat scripts/mods/build-npc-port-pursuer.nr.kts --port-dir=<port dir> --slug=<slug> --work=<dir> --keep-work=1 --npc-ports=<work>/np
    
  4. Applies TAE fix TSVs to the built anibnd with ds3-arena/patches/apply_tae_checked.py.

  5. Runs pursuer/py/pose_cancels.py (attack cancels by pose) and applies its TSV the same way.

  6. ds2build.py only: normalises Kraken output with dcxnorm.py and applies ai-port/hidden_guard.py to the battle luabnd.

  7. Runs port-lint.exe on the package and writes reports/port-lint-final.md and .tsv.

What the Kotlin script does

scripts/mods/build-npc-port-pursuer.nr.kts, one stage after another. A failing stage stops the build.

Stage Tools What it makes
extract Ds2Unpack, NrUnpackCli, Ds2Port bnd-x the DS2 model, rig, TAE, sound files and the NR template files
model Ds2HumanPort flver, Ds2HumanPort tex, material-fix/apply_tex.py the FLVER and the _h / _l texbnds
anim Ds2Port flver-bind, Ds2Port nsa-dump, anim_build.py, tna1.py the hkx clips and parts.json
tae Ds2HumanPort attacks, Ds2HumanPort tae the TAE and <chr>.events.tsv
behaviour Ds2HumanPort anibnd, AddLayers additive, Ds2HumanPort behbnd, Ds2HumanPort ragdoll, Havok2014 relayout, Ds2HumanPort chrbnd, Ds2Port hks, Havok2014 tagcheck anibnd, behbnd, chrbnd, hks
ai AiPort localize, then aicheck and luabnd-pack in Kotlin the battle luabnd
sfx pursuer/fx/build_fx.mjs (Node) with FxBnd the chr's sfxbnd, skipped when fx_spec is null
sound py/build_sounds.py, optional bank-budget/bank_budget.py the chr's sound bank, and boss music unless music is false
params StatPort port, Ds2HumanPort params test-only/regulation.bin and params/rows.csv
port-lint port-lint fix, port-lint fixes in place, then the lint report

Script options, from its header: --ds2-game, --nr-game, --nr-unpack-cli, --vgmstream, --python, --node, --soulstruct-lib, --ds2-defs, --work=<dir>, --keep-work=1, --no-music=1, --strict-lint=1, --no-port-fix=1, --port-dir=<dir>, --slug=<s>, --npc-ports=<dir>.

Output goes to S:/souls-script-kt/output/npc-port-<slug>/: mod/, params/, test-only/, reports/.

The tools

All three build with EB tool build ds2humanport ds2port ds2unpack. In a port build you never call them by hand: npmirror.py places them.

Ds2HumanPort

Edit it in S:/eclipse-build/tools/dotnet/Ds2HumanPort. Usage text from Program.cs:

tae-dump <anibnd|tae> [--ids a,b] [--template TAE.Template.ERNR.xml]
flver --ds2 <c####.flv> --ref <NR template chrbnd> --out <out.flver> --src c#### --dst c#### [--dummies <json>] [--glow]
dummies <in chrbnd> <out chrbnd> <dummies json> [--only id,..]
tex --ds2 <ds2 texbnd> --out <mod dir> --src c#### --dst c#### --suffix _h|_l [--scale N] [--verify]
attacks <enc_regulation.bnd.dcx> <ds2 defs dir> <ds2 _pl.tae> <slots.json> <out dir> [--verify]
tae --config <tae.json>
behbnd --template <NR behbnd> --template-anibnd <NR anibnd with skeleton.hkx> --from c#### --chr c#### --skeleton <skeleton.hkx> --out <mod dir> [--bone T=N ...]
chrbnd --template <NR chrbnd> --from c#### --chr c#### --flver <flver> --ragdoll <hkx> --out <mod dir>
anibnd --template <NR anibnd> --from c#### --chr c#### --hkx <dir> --tae <tae> --out <mod dir>
ragdoll <ds2 ragdoll.hkx> <donor NR chrbnd> <skeleton.hkx|bind.json> <out.hkx> [--verify] [--name c####]
param-dump <regulation.bin> <Param> <id|lo-hi>...
matscan <dir of chrbnds> <matbin name>
bndcheck <binder>...

The code also has params and tex-dump, which the usage text leaves out. The Kotlin script calls tae with separate options (--ds2-pl, --ds2-snd, --ds2-sfx, --template, --parts, --attacks, --slots, --fx, --chr, --out, --music-stop, --stop-music-on, --tpl-alias, --bullet-dmy, --extra, --patch, --sound-map, --blend-nr, --turn-fill), not --config. The header of TaeBuild.cs explains what each does.

Ds2Port

The commands the route uses: bnd-x <bnd[.dcx]> <outDir>, flver-bind <flver> <out.json>, nsa-dump <ds2 anibnd> <outDir> (set NSA_MAPOFF=1 for c6790), hks <tpl.hks> <tplChr> <chr> <modDir>. The full list is in reference.md.

Ds2Unpack

Ds2Unpack --game <DS2 SotFS Game dir> --out <dir> [--include <prefix>]... [--list]

--dictionary <file> names the UXM Scholar dictionary. Its default is a path in a Downloads folder. The tool writes Assets/UXM Dictionaries/ into its own folder on every run, so npmirror.py gives each build a private copy of it. AddLayers gets a private copy for the same reason.

The DS2 Python steps

tools/ds2/py/pursuer/ holds anim_build.py, tna1.py, pose_cancels.py, build_sounds.py, build_throw_receive.py, ds2_throw_receive.py, make_tae_plan.py, nrclip.py, fev44.py. tools/ds2/py/aava/ holds hk_build.py, build_sounds.py, audio_prep.py, fev44.py. npmirror.py maps them over npc-ports/pursuer/py and npc-ports/aava/py in the mirror. Scripts shared with the DS1 and DS3 builds (apply_tae_checked.py, crit_fix.py, hidden_guard.py, the bank builders) still live in souls-script-kt.

Environment switches

Variable Read by Effect
EB_DHP_LIVE=1 npmirror.py Ds2HumanPort only is a private build of the souls-script-kt source npc-ports/pursuer/Ds2HumanPort, into <work>/np-dhp-bin. For trying an edit there.
EB_TOOLS_LIVE=1 npmirror.py No swaps at all. Every tool is the live souls-script-kt bin, and Ds2HumanPort is the private live build.
EB_NO_CACHE (any value) the Kotlin script The anim stage runs anim_build.py then tna1.py directly, without the cache.
EB_FIX_CHAIN=<file> Ds2HumanPort tae The fix-chain file to read. Fails if the file is missing. See the traps.
ECLIPSE_NO_ANIM_OPS=1 set by ds2build.py --no-anim-ops Skips the chr's anim-ops.tsv lines.

The anim stage cache

With S:/eclipse-build/eb.py and tools/ds2/anim_stage.py present and EB_NO_CACHE unset, the Kotlin script runs the anim stage as one cached step:

py -3.12 -I -B S:/eclipse-build/eb.py cache run --mem 3 --py <python> --in <inputs...> --out <hkx dir> <parts.json> -- py -3.12 -I -B anim_stage.py --python <exe> --soulstruct-lib <io_soulstruct_lib> --anim-build <anim_build.py> --tna1 <tna1.py> --out-dir <hkx dir> --tna1-args "<args after --dir>" -- <anim_build args except --out>

The inputs it names are the NSA dump, bind.json, the compendium folder, animmap.csv, the three Python scripts, hk_build.py, the TNA1 converter folders, the Ds2Port bin folder and the two Soulstruct source folders. A hit restores the hkx files and parts.json. patches/README.md records the measurement: 113 to 176 s uncached, 1 to 2 s on a hit, on the Rotten.

Known traps

  • Death fade twice on chain characters (state unclear). Ds2HumanPort tae adds a default death fade unless the chr is listed in ds3-arena/patches/fix-chain.tsv. It finds that file at <exe folder>/../../../ds3-arena/patches/fix-chain.tsv, or at EB_FIX_CHAIN. The 2026-11-07 memory note says a junctioned bin folder made .NET resolve the exe folder outside the mirror, so the file was not found and chain chrs (c9470, c9530, the Pursuer, DS1 and DS3 chain ports) got the fade twice. The fix then was to make the bin a real folder. Today npmirror.py makes that bin a junction to the eclipse-build folder again, and no build script I read sets EB_FIX_CHAIN. I could not prove from code which way .NET resolves it. Before staging a chain chr, set EB_FIX_CHAIN to the real file and diff the result against the shipped anibnd.
  • Dummies 960. The death dissolve spawns on dummy set 960. port-lint fix adds the set and the dissolve (PortLint/DeathDissolve.cs). The memory note says shipped 0.0.82 Last Giant and Flexile Sentry FLVERs lacked them. Not rechecked here.
  • Two anim builds at once. Keep the private APPDATA step in any new build.py.
  • Tools that write into their own folder. Never point a build at the shared eclipse-build folder of Ds2Unpack or AddLayers. Go through npmirror.py.
  • DS2 clips at about 20 fps. anim.fps_out in port.json resamples them. Without it they play 1.5 times too fast (script header).
  • CritFix changes the current directory to a hard-coded souls-script-kt path (kneel-retarget\bin) to find Oodle.

Read DS2's clip usage with the morpheme MCP

DS2 uses morpheme networks (.nmb), not a Havok behaviour graph. The port reads only the rig and clips from them, so it does not know how DS2 blends a clip. Look it up first.

  • The MCP server is registered as morpheme. Source: S:/_modding/tools/DS2AnimToolset/mcp/morpheme_mcp.py.
  • Tools: export_network (takes an anibnd.dcx), network_summary, list_anims, anim_usage, node, find_nodes, export_dir.
  • Without an MCP client: py -3.12 -I morpheme_mcp.py --call anim_usage '{"chr":"c6790","anim":"DmgBossHit"}'.
  • The decompiler behind it: morphemeNetCli.exe <cXXXX.nmb> <out dir> [--fps 30] [--resample]. The .nsa clips must sit beside the .nmb.
  • Cache: S:\_modding\tools\DS2AnimToolset\mcp-cache\<chr>\ (override with MORPHEME_MCP_CACHE).

Example from the MCP's README: Aava's DmgBossHit is only used in additive layers on top of the main motion. Ported as a full-body clip it reads as a freeze.

The toolset clone has no license. Keep it local.

Dated note (memory, 2026-11-07, not rechecked): the audit kit is S:/souls-script-kt/scripts/tools/npc-ports/_ds2audit (anidiff.py, taediff.py and others). Use anidiff.py and taediff.py to compare a rebuild with the shipped files.

Source files this page documents (11)
  • eb:tools/ds2/
  • eb:tools/dotnet/Ds2HumanPort/
  • eb:tools/dotnet/Ds2Port/Program.cs
  • eb:tools/dotnet/Ds2Unpack/Program.cs
  • eb:tools/dotnet/DS2-FAMILY-IMPORT.md
  • eb:toolchain/python.py
  • eb:patches/README.md
  • sskt:scripts/mods/build-npc-port-pursuer.nr.kts
  • sskt:scripts/tools/npc-ports/ds2-belfry-gargoyle/ds2build.py
  • sskt:scripts/tools/npc-ports/ds2-heide-knight/build.py
  • modding:DS2AnimToolset/mcp/

Changed one of these? Update this page: ports/port-a-ds2-character.md (how).