Team Cereustooling wiki

Edit map files from the command line

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

These tools read and change map files (MSB, map FLVER and texture packs, collision, navmesh, event scripts) without opening an editor. For work inside Smithbox see ../smithbox/generate-navmesh.md and ../smithbox/port-er-map.md.

Checked against code: every command name and argument below, from the comments and argument handling in each tool's source. Not checked: what each command does inside, beyond its own comment. Nothing was run. In-game results are stated only where a source says so.

EB means py -3.12 -I -B S:/eclipse-build/eb.py. Build a tool with EB tool build <name>, run heavy steps with EB run --mem <GB> -- <command>. See ../build/use-shared-tools.md.

Uncommitted work in progress

git -C S:/eclipse-build status --short on 2026-11-12 shows these tool files changed but not committed. Their behaviour can still move:

File What the uncommitted part is
tools/dotnet/MapKit/Program.cs the new farcheck command, and addfar now sets the far-part invariants and runs farcheck
tools/dotnet/TerrainCut/Program.cs, HkxCut.cs, ExternCut.cs cut --collision zero|dead|dup, and the fields and audit commands
tools/dotnet/TerrainCut/Audit.cs new file, not tracked yet
tools/dotnet/CastleVariants/Build.cs the boss name fix now also covers the evergaol kill call 90045002
tools/dotnet/CastleVariants/Variants.cs the opt-in variant key reuseDummyVar

Because a tool's folder is named by a hash of its source, EB tool build on this working tree builds the uncommitted version.

MapKit

EB tool build mapkit (MapKit.exe). MapKit makes paths absolute and then changes to its own folder. The tools.toml comment lists five commands; the code has 25.

Look at files (read only)

Command Arguments Prints
flvermats <geombnd|flver ...> per FLVER material: name, MTD or matbin, texture slots
flverinfo <geombnd|flver ...> header fields, buffer layouts, and per mesh: material, vertex buffers, face sets, bounds
tpfls <tpf.dcx|texbnd ...> texture names inside TPF files, also TPFs inside a binder
tpfinfo <tpf.dcx ...> per texture: format, size, mips, and whether the DDS size adds up
bndnames <bnd ...> entry names of BND4 / BXF4 files
matsamplers <matbinbnd> <names.txt> per listed matbin: shader, then each sampler
msbparts <msb.dcx> model, part, region and event counts; enemy and chest tables
dumpall <msb.dcx> every model, part, region and event with all properties, one KIND<TAB>type<TAB>name<TAB>key=value;... line each
farcheck <block.msb.dcx> [...] PASS or FAIL per file for the far-part invariants; exit 1 on a failure (uncommitted)
verifyvar <base.msb> <merged.msb> <N> checks that the merged map, seen as each base variation, equals the base

Change binders, models and textures

Command Arguments What it does
matadd <base matbinbnd> <source matbinbnd> <out matbinbnd> <name-list.txt> copies the listed matbin entries from source into base, by entry. New id = highest id + 1. Refuses a name base already has. Writes DCX_KRAK.
rewrap <in.dcx> <out.dcx> reads a BND4 and writes it again as DCX_KRAK with a padded file end, which eb dcx normalize needs
dcxraw <in.dcx> <out> writes the decompressed bytes
tpflow <in.tpf.dcx> <out_l.tpf.dcx> makes the _l texture pack: two top mips dropped, textures renamed <name>_l
flverver <in geombnd.dcx> <out geombnd.dcx> <hex version> overwrites the version field of every FLVER entry, nothing else
flvermtd <in geombnd.dcx> <out geombnd.dcx> strip_er | all=<name> renames every material's MTD: drops _er, or sets all to <name>.matxml. For in-game probes.
flvernr <in geombnd.dcx> <out geombnd.dcx> rewrites an Elden Ring asset FLVER (0x2001A) in Nightreign's asset layout (0x20021): one vertex buffer per attribute, LOD face sets present
copyasset <src aeg root> <dst aeg root> <from id> <to id> copies an asset's files to a new asset id

The flvernr comment says ER-layout assets draw in small maps but not in m60 tiles. That is a source comment, not rechecked here.

Change an MSB

Command Arguments What it does
campmerge <base.msb> <vanilla.msb> <edit.msb> <out.msb> <config.tsv> adds someone's edit of a small POI map as a new variation on the shipped map
partremodel <in.msb> <out.msb> <variation> <part>=<TO> ... moves an asset part of that variation to model TO
partedit <in.msb> <out.msb> <variation> <edits.tsv> edits asset parts of one variation
tileunk <in.msb.dcx> <out.msb.dcx> <value> <model prefix> sets TileData.Unk0C on every Asset part whose model starts with the prefix
addasset <in.msb.dcx> <out.msb.dcx> <template asset part> <spec> [<spec>...] one new Asset part per spec, a deep copy of the template. spec = MODEL,x,y,z,yaw[,unk0C]
addfar <in.msb.dcx> <out.msb.dcx> <template asset part> <spec> [<spec>...] far (LOD block map) copies of tile assets. spec = NAME,MODEL,x,y,z,rx,ry,rz,scale
keepparts <in.msb.dcx> <out.msb.dcx> <keep.txt> a stripped copy: parts not named in keep.txt go, with the events that name them and unused models

campmerge config lines, tab separated, # comments:

var <N>                                   new variation number
remodel <FROM=TO,FROM=TO>                 parts of variation N on model FROM move to model TO
partmodel <part>=<TO>[,<part>=<TO>...]    one part moves to model TO and is renamed <TO>_<instance id>
partdrop <part>[,<part>...]               remove these parts of variation N (run before partmodel)
enemypos <part> <x> <y> <z>               move an enemy part
enemy <part> <npcParam> <npcThink> <backupAnim>
chest <part> <objActParam> <lotId> <flag> Treasure + ObjAct events for an added chest or corpse
clonechest <orig part> <flag> <lotId>     own flag and lot for the event clone of a split part

In campmerge, base is the map as shipped now, vanilla is the game's MSB the edit started from, and edit is the other person's MSB. Vanilla parts the edit removed or changed become variation 0 and get a clone for every other variation.

partedit lines, tab separated:

model <part> <TO>
drop  <part>                   refused when an event or region names the part
set   <part> <Property> <v>    int, float, bool, or x,y,z for Position, Rotation, Scale

addfar names: NAME must be <owner tile>-<exact owner part name>, for example m60_42_38_60-AEG023_260_7028. The tile is the layered owner tile. From the name the command sets the instance id, TileData.MapID, TileData.Unk14 and PartName4/5/6, then runs farcheck and fails if it does not pass. Run it on a block MSB that already passes farcheck.

farcheck tests three things on a LOD block MSB: instance ids over MapPieces and Assets are exactly 0 to N-1; each far part's Unk14 equals the number after the last _ of its name and its MapID is the tile in its name; PartName4/5/6 name the part itself.

TerrainCut

EB tool build terraincut. Cuts openings (tunnel mouths, shafts) through map terrain, in the collision and the drawn mesh.

TerrainCut ls    <file>
TerrainCut tris  <file> <out dir> [--entry s ...] [--all-lods]
TerrainCut cut   <in> <out> (--box B ... | --boxes <file>) [--entry s ...] [--clip] [--collision zero|dead|dup] [--report <file>] [--excess-obj <path, {stem} = entry>]
TerrainCut check <orig> <cut> (--box B ... | --boxes <file>) [--entry s ...] [--margin m] [--tris <dir>]
  • A file is an h/l/f .hkxbhd (its .hkxbdt beside it), a .mapbnd.dcx or any BND4, or a single .hkx(.dcx) or .flver(.dcx).
  • B = cx,cy,cz,hx,hy,hz[,yaw]: centre, half-extents, yaw in degrees about +Y, in the part's model space. Terrain parts sit at the tile origin with no rotation, so that is tile-local. Check the MSB part transform.
  • --boxes: one B per line, # comments. --entry: entry-name substrings to cut. Default is every entry with a triangle in a box.
  • Collision: primitives that reach into a box are taken out in place. --collision zero (default) makes a zero-area point, dead writes a DE AD DE AD marker, dup copies the nearest kept triangle. This option is uncommitted.
  • Drawn mesh: triangles that reach in are removed, or cut at the box faces with --clip.
  • --excess-obj writes what the cut took away outside the boxes as an OBJ, to rebuild as asset collision.
  • check re-decodes both files. It fails (exit 1) if a triangle with area reaches into a box or a triangle farther than the margin (default 0.5) changed.

The code also has rewrite, fields <file> [--entry s] and audit <file>... (fields and audit uncommitted). audit prints the structural health of every collision mesh in a binder. They have no usage text; see Program.cs.

CastleVariants

Two builds exist: castlevariants is the frozen 0.0.86 binary the release pipeline uses for fmgadd, and castlevariants_src is built from tools/dotnet/CastleVariants and has the newer operations. Use EB tool build castlevariants_src for map work. How a seed uses it is on ../seeds/put-a-seed-in-the-mod.md.

The tool's own help line:

castle-variants: msb | rows | cmp | atk | emevd | build msbjson|apply | names | matbins | matdiff | verify align|msb | arena dump|apply | testevent stonesword | fmgls | fmgadd

Commands with arguments I could read:

Command Arguments What it does
build msbjson <mapstudio dir> <out.json> <map>... vanilla enemy parts of the maps as JSON
build apply <edits.json> MSB part swaps, clones and drops plus EMEVD name and music arguments, from the vanilla maps. The edits.json format is written by the seed planner and is not written up here.
fmgls <msgbnd> every FMG with its entry count and id range
fmgadd <msgbnd in> <msgbnd out> <tsv> adds FMG entries. TSV lines: <fmg file name>\t<id>\t<text>; a fourth column replace overwrites an id that holds other text
verify align <dir>... every BND4 in a .dcx under the folders has 16-byte aligned entries
verify msb <dir> every enemy and dummy part of every MSB under <dir>\map\mapstudio has a model entry
arena dump <msb.dcx|-> <emevd.dcx> <out.json> the Boss Arena map's enemy parts and events
arena apply <spec.json> applies part and event edits; refuses when the current state is not what the spec expects
rapappend <regulation in> <rap-rows.tsv> <regulation out> appends RandomAppearParam rows without re-sorting
matdiff <binder A> <binder B> entries by file stem: same, different, only in A
gaolgate <in emevd> <out emevd> <common_func emevd> <variation,...> adds the gaol prompt gate for those variations
testevent stonesword <in common.emevd.dcx> <out common.emevd.dcx> [flag] [lot] [event] test overlay only, never a release
rows <regulation> <Param> <id>... every cell of those rows
dcxd <in> <out> decompressed bytes

msb, msball, cmp, atk, emevd, emevdp, names and matbins are inspection commands; see Inspect.cs.

A POI variant never overwrites a vanilla variation. build apply copies a vanilla variation into a new variation number and the seed's own pattern row loads it. A tile carries up to 10 variations (Variants.cs).

AddLayers

EB tool build addlayers. A character tool that lives here because it is a small binder editor:

probe <tae anibnd> <id-lo> <id-hi> [hkx binder...]
additive <in anibnd> <donor tae anibnd> <out anibnd> [donor div binder...]
twist <in behbnd> <out behbnd> <property=boneIndex>...

additive gives a port the additive layer clips (9000 to 9999) of the vanilla graph it runs on. It writes a temp file into its own folder, so builds use a private copy (see port-a-ds2-character.md).

POI template maps

tools/camp-py/poi_templates/build.py strips each main POI map to its ground, to build a new POI on.

py -3.12 -I build.py <mapkit.exe> <release mapstudio dir> <vanilla mapstudio dir> <out dir>
  • For each of 29 POI maps it takes the map from the release if the release ships it, else from vanilla.
  • It runs MapKit dumpall, lets classify.py pick the parts to keep, then runs MapKit keepparts.
  • Kept: map pieces, collision, the player start, the storm effects, and rock and ground assets. Removed: buildings, props, vegetation, enemies and the events that name them.
  • Output: <out>/map/mapstudio/<map>.msb.dcx, <out>/keep/<map>.txt, <out>/README.md.
  • classify.py reads poi_models.json (per asset model: size and materials) to tell ground and rock from the rest.
  • The map's navmesh files are not changed. They still describe the original POI.
  • The README text it writes names "Eclipse 0.0.104" as the release. That string is fixed in the script.

Collision triangles in Python

tools/camp-py/hknp_tris.py reads the triangles of a compressed collision mesh from the text of Havok2014 dump18 <hkx> --all:

from hknp_tris import tris_from_dump
T = tris_from_dump(text)   # numpy (N, 3, 3), in the asset's part space

It needs numpy. --all is not in Havok2014's usage text, but dump18 accepts it (full depth dump). The script's header lists the two decoding rules that earlier stage copies got wrong: the shared-vertex bit widths, and the body's own rotation and position.

EB tool build havok2014. Havok2014 nav prints this usage. A spec is file.hkx or binder.dcx::entry.

navinfo  <spec> [--faces-out tsv] [--points file] [--reference spec] [--hklib-roundtrip]
navcover <spec> --footprints f [--agent-radius 0.4] [--step 0.1] --out tsv
navunlink <spec> --out hkx [--footprints f] [--agent-radius 0.4] [--min-cover 0.5] [--faces list]
         [--max-free m2] [--keep-points file [--keep-points-dy 0.5]] [--keep-connected [--min-region 4]]
         [--step 0.1] [--clearance recompute|keep] [--report tsv]
navclearance <spec>
Command What it does
navinfo counts, link consistency, walkable regions. --points takes name x y z lines and prints the face under each. With --reference, only edge links and clearance values may differ, else FAIL.
navcover per face, the fraction covered by the footprints
navunlink unlinks every face covered at least --min-cover, keeping face indices stable. --max-free: a face keeps its links when more than this much of it is uncovered. --keep-points: the face under each point keeps its links. --keep-connected keeps a face whose cut would split a free region.
navclearance recomputes every clearance value and compares with the stored ones

Footprints file: a part <name> line, then ring x,z x,z ... lines. A sample point is covered when it lies inside a part or within the agent radius of one of its rings.

Dated notes (memory nr-navmesh-route, 2026-11-11):

  • One navmesh serves every variation of a map. Cutting it for one variation costs the others free ground.
  • To make enemies path around props, set the asset param field navimeshFlag on the blocking props instead. Stan saw this work in game on the Overgrown Boneyard camp on 2026-11-11.
  • The unlink route (stage camp-navmesh-097) was not proven in game.
  • A generated navmesh from Smithbox is unproven in game. See ../smithbox/generate-navmesh.md.
Source files this page documents (7)
  • eb:tools/dotnet/MapKit/
  • eb:tools/dotnet/TerrainCut/
  • eb:tools/dotnet/CastleVariants/
  • eb:tools/dotnet/AddLayers/Program.cs
  • eb:tools/dotnet/Havok2014/Nav18.cs
  • eb:tools/dotnet/Havok2014/Program.cs
  • eb:tools/camp-py/

Changed one of these? Update this page: ports/map-tools.md (how).