Repository navigation
Releases: UberMorgott/PhoenixPoint-Mod-ContentTool
Release list
ContentTool 1.4.1
Install: unzip into Phoenix Point\Mods\ (gives Mods\ContentTool\ContentTool.dll + meta.json), tick ContentTool in the mod manager. Full guide: https://ubermorgott.github.io/PhoenixPoint-Mod-ContentTool/
Fixed
- Mod weapons stay visible and fire in tactical missions after leaving the geoscape; before, they could come back invisible or without their fitted shoot point (
NoValidTarget). - Creatures built from a model file shoot from their weapon's muzzle; before, the shot started up to ~0.8 m off the barrel.
- A creature's melee Strike finds its targets, including when the Strike comes from an inherited animation set (it used to always answer
NoValidTarget). - The bench 3D view no longer renders black with post-processing upscalers such as Renderforge DLAA enabled.
- Add a weapon:
Revertrestores the weapon's rotation and size whenppcontent.jsonleaves them out; auto scale re-solves after a turn and stays automatic onSave fit; the paste-ready fit report no longer writes the auto scale as a fixed value. - Replacing a model works on ContentTool creatures whose mesh lives on the rig.
ContentTool 1.4.0
Install: unzip into Phoenix Point\Mods\ (gives Mods\ContentTool\ContentTool.dll + meta.json), tick ContentTool in the mod manager. Full guide: https://ubermorgott.github.io/PhoenixPoint-Mod-ContentTool/
New
- The bench looks like the game: its font (Purista), buttons, frames, cards, step bar, main buttons and
PASS/WARN/FAILbadges come from the game's own widgets and sprites, and every field, toggle, slider and scroll bar wears the same look. - The bench scales to any resolution and window mode (windowed, borderless, fullscreen): the whole layout, the 3D gizmo, the Model Doctor's bone inspector, skeleton markers and legend grow and shrink with the window height, also while the window is resized.
- Add a weapon starts with an Assault soldier holding a Phoenix assault rifle and playing the game's idle, instead of whatever the previous screen left on the platform.
Fixed
- A content mod's very long build line no longer reaches the game's own log sink in one piece, which caused the startup error
Mesh can not have more than 65000 vertices. - The bundled demo mods are re-baked with the current ContentTool (their
Distbundles were baked by 1.0.0).
ContentTool 1.3.0
Install: unzip into Phoenix Point\Mods\ (gives Mods\ContentTool\ContentTool.dll + meta.json), tick ContentTool in the mod manager. Full guide: https://ubermorgott.github.io/PhoenixPoint-Mod-ContentTool/
New
- Redesigned bench: home screen with six task cards,
Home/< Back/ breadcrumb navigation (Escgoes back), clickable completed steps, a right-hand pane per task, selectable logs withCopy log/Open log folder. - Sounds screen: search and preview game sounds and your own file, then
Add to mod & build. - Videos screen: preview game clips and your own (pause / stop / seek), replace a game video or add a new one.
- Add a weapon screen: pick weapon kind + donor, your
.glband a name, build, fit it in the hand with move / turn / size sliders,Save fit, check the kind's moves. - Fit a weapon screen: adjust a mod weapon in a soldier's hand with drag handles and save to
ppcontent.json. - DLL-less content mods build their
weaponsrows when enabled;ct_voicesandct_mission listare public;meta.jsonaccepts comments and trailing commas.
Changed
Replace a modelbuilds on aWARNverdict too; onlyFAILblocks.Build & shareshows the log beside the steps and can run a single stage.- Faster loading with many mods: patched copies reuse the shipped compressed blocks (LZ4 fast repack as fallback), ticking a mod on runs a patch-only bake, failed bakes are not repeated every launch.
- Conflicting claims on the same bundle / video / sound media ID are resolved by mod ID order.
Fixed
- Bad
ppcontent.json, missing/ambiguous videos and misspelledct_extractnames give clear refusals instead of exceptions. - Replaced meshes carry tangents (donor normal maps shade them);
_EmissionColoris sRGB-encoded. - Mixed-case mod IDs no longer get model keys refused; two weapons cloning the same donor both load.
- Content published by a mod whose DLL fails while enabling is taken back; creature builds no longer modify borrowed donor animation defs.
Full list: https://ubermorgott.github.io/PhoenixPoint-Mod-ContentTool/whats-new/
ContentTool 1.2.1
ContentTool 1.2.1 lists shipped sounds by name and extracts loose audio as WAV files so you can find the sound you want to replace.
Find a sound by name or id
ct_list audio [filter]now shows every shipped sound by name, read at runtime from the game's ownSoundbanksInfo.xmlin<install>\PhoenixPointWin64_Data\StreamingAssets\Audio\GeneratedSoundBanks\Windows\.- Each row is
<id> <name> <bank> loose|in-bank, for example:6372602 5_IND_Taunt_03 Barks loose. - The filter is a case-insensitive substring of the name, the id or the bank:
ct_list audio taunt,ct_list audio 6372602,ct_list audio Barks. - Media in several banks show their bank names joined with
+and match any of them. - The list covers 7691 named media: 3105 loose
.wemfiles are extractable; 4591 inside.bnksoundbanks are listed asin-bankso you can find their ids, but are not extractable. - The pane shows the first 10 rows and then names a file; the whole list is always written to
<persistentDataPath>\ContentTool\Logs\ct_list-audio-<stamp>.txt. A capture such as PPCLI still receives every row. - If the XML is missing, the header ends with
- NO NAMES: <reason>and rows fall back to numeric names.
Extract one sound or all loose audio
ct_extract audio <mediaId>now also writes<ShortName>__<id>.wavbeside the byte-for-byte.wem.- Use
ct_extract audio --all [filter]to decode every loose media the filter matches into...\ContentTool\Extracted\audio\<ShortName>__<id>.wav. - Bulk extraction writes
index.csvwith columnsid,shortName,bank,loose,wav, covering all media including the in-bank ones. - Progress is reported every 200 files. The whole library is 3105 decodes.
- The final line is
extracted N of M loose media (K in-bank skipped) into <dir>. - A failing decode is counted and named, never fatal.
Use the media id for replacements
- The Wwise MEDIA id is the number in a loose
.wemfilename. It is what ContentTool replaces and what these commands take. - The Wwise EVENT id is
AK.Wwise.Eventin the game's defs. It is never a loose filename. - If the only id you have is an event id, search the sound's name in the list instead.
- Replacing a sound is unchanged: the manifest row and the file still use the media id.
Follow the updated guides
- The sounds guide now covers finding and extracting sounds. The old
ct_list audio confirmexample is gone: it could never match anything because the list held only numbers. - The find-content guide has also been rewritten.
Keep long output visible
- The game's mod logger creates a console text object with shadow per call. One call over roughly 8 000 characters throws
Mesh can not have more than 65000 vertices, losing the frame's console geometry. Splitting at 8 000 characters still failed: five parts, five throws. - ContentTool now sends one bounded message per command, at most 5 000 characters: verdict head,
... N line(s) not shown, last lines includingct_project: …, and the full-text file path<persistentDataPath>\ContentTool\Logs\log-<stamp>.txt. The console pane shows the same bounded text.ct_list audiokeeps 10 rows plusct_list-audio-<stamp>.txt; a capture such as PPCLI receives every line. - Verified in game: long-verdict
ct_project,ct_list audio, and enabled project startup bake; zero exceptions, trailer visible.
Install
- Unzip so that
Mods\ContentTool\ContentTool.dllandMods\ContentTool\meta.jsonexist under your Phoenix PointModsfolder. - Enable
ContentToolin the in-game mod manager, then restart. - Mods that depend on ContentTool list
com.morgott.ContentToolin theirmeta.jsonDependencies.
ContentTool 1.2.0
ContentTool 1.2.0 adds an in-game bench to check replacement meshes and take a project from validation to packaging.
Work from the loaded game
- Open the bench with
ct_bench openfrom a loaded geoscape, or pressCtrl+Alt+B. FITkeeps the weapon-fit tools;MODEL DOCTORandLIFECYCLEare new.- The bench guide covers the three tabs.
Check a mesh before you bake
- Model Doctor compares your replacement mesh against a shipped prototype slot.
- Pick a prototype, a variant and a slot, then choose your GLB with
Browse.... - There are now 350 variants across 36 prototypes. Armour sets have separate variants, such as
Human / PX_HeavyStarting. - Before 1.2.0, only one variant per manager was listed, which left most armour sets unreachable.
- The header tells you whether the result is
BY NAME - your weights will be used, nearest-bone, not rigged, orIMPORT REFUSED (N reason(s)). - Rows name the problem: missing or extra bones, a 1-triangle shard where the target has two materials, no armature, or a rigged file on a static target.
- Foreign rigs, including Khronos sample models, never get a false
BY NAMEand never throw. They are refused or downgraded with the reason. - In the SHIP section,
CREATE, BAKE & APPLYwrites a ready mod folder beside ContentTool withppcontent.json,meta.jsonand your GLB, then bakes it and applies it live. - The Model Doctor guide covers the checks and shipping step.
Run the project loop in one panel
- The Lifecycle tab has
Validate,Bake,Apply,VerifyandPackagerows. - Use
Run allfor the whole loop, per-rowRunfor one stage, orCancelto stop. - The status line only shows what the stages said.
Validatenow catches a texture outsideContent\Textures\, a mesh outsideContent\Meshes\, and two source files with the same stem offline, before bake time.- A
.glbwithout an armature is reported as a note during validation. - Apply reports
restart requiredwhen the game already holds an older copy of the bundle. Verify refuses until you restart. - Package writes to
%LOCALAPPDATA%\ContentTool\Packages\<projectId>\<yyyyMMdd-HHmmss>-<runId>. - The panel uses the same producers as
ct_project,ct_route7 apply,ct_route7 verifyandct_package. Those console commands are unchanged and still documented. - The Lifecycle tab guide covers each stage.
Follow a real armour replacement
- The worked armour-set example takes a PX Heavy torso, both legs and albedo from a Blender export to the final green run.
- It includes the mistakes the tool caught: no armature, a nearest-bone export, a texture in the wrong folder, a 1-triangle shard, and a texture row naming an asset that does not exist.
Keep the verdict visible and the bench usable
- A suspicious submesh, such as a tiny shard beside a real part, is now a WARNING instead of a bake failure. The run still tells you about it.
- Long
ct_projectverdicts no longer blank the in-game console. At most 60 lines are printed, and the last lines, includingALL PASSorN FAILURE(S), always survive. - The full verdict goes to
<persistentDataPath>\ContentTool\Logs\console-<stamp>.txt; the console names the file. - Bench gizmos and handles are drawn at output resolution from OnGUI, removing ghost trails under temporal upscalers such as DLSS via Renderforge.
- The Doctor header no longer clips mid-word. Expected import refusals produce one warning line instead of an error with a stack trace.
Cancelstays reachable while a stage waits for the panel to paint. Switching tabs no longer trips an IMGUI layout error.
Install
- Unzip so that
Mods\ContentTool\ContentTool.dllandMods\ContentTool\meta.jsonexist under your Phoenix PointModsfolder. - Enable
ContentToolin the in-game mod manager, then restart. - Mods that depend on ContentTool list
com.morgott.ContentToolin theirmeta.jsonDependencies.
ContentTool 1.1.3
A reliability release. Nothing you write changes; what the tool tells you does.
Runs that used to die now finish, and name what was wrong
- A
replacerow pointing at an asset that is not in the bundle used to kill the whole bake — no per-row line, no summary, and the rows already patched went unreported. Now that row alone is REFUSED, by name, and the line tells you to runct_list assets <bundle> <type>to see what the bundle actually contains. All four kinds are covered: materials, animation clips, meshes, textures. - One malformed
replace,soundsorpublishrow now costs you that row instead of the whole run. - A mistyped bundle name gives a plain REFUSED line instead of a
FileNotFoundException. - A bad sound row no longer stops the bake at the first one — the run finishes and still cleans up after itself.
The tool no longer claims success for work it did not do
- Every refusal is counted, so a run with omitted work can no longer end in ALL PASS.
- A partial patch no longer calls itself "the last good bake", and no longer stamps the cache.
- Same-name collisions and a negative
scaleare refused by name. - A sound another mod already owns, and a video that never actually served, now count as failures instead of quietly passing.
- The bundle-redirect success line now says the change takes effect on the next load, rather than implying it is already live.
Packaging no longer poisons your output folder
- An empty or brace-unbalanced
ppcontent.jsonused to package as a success. It is now refused. - A
soundsrow with a file extension the tool does not accept now trips the NEVER BAKED check instead of being silently skipped. - An IO error partway through a copy used to leave a half-staged output folder that permanently refused every later run. It cleans up now.
Verified in a live game (build 76bf7b48): all 12 shipped demos re-baked with no pass-to-failure regression, and each fix confirmed against its own output line.
Install
Drop ContentTool.dll and meta.json into a ContentTool folder under the game's Mods directory, replacing the previous version.
ContentTool 1.1.2
Published after 1.1.3 to fill a gap in the numbering. 1.1.3 is the newest release — use that one.
A missing replace target no longer kills the bake
- A
replacerow pointing at an asset that is not in the shipped bundle used to abort the entire bake: no per-row line, noct_project: N FAILURE(S)summary, and the rows that had already been patched were reported nowhere. It looked like the tool simply gave up. - Now every replace kind — material, animation clip, mesh, texture — checks the target first. If it is missing or ambiguous, you get a REFUSED line naming that row, pointing you at
ct_list assets <bundle> <type>to see what the bundle really contains. The row is counted as a failure and the run continues with the rest of your rows.
Install
Drop ContentTool.dll and meta.json into a ContentTool folder under the game's Mods directory, replacing the previous version.
ContentTool 1.1.1
A verification patch on top of 1.1.0. No behaviour of the importer or the baker changed; what changed is what is now measured, and one demo's documentation that was wrong.
Corrected numbers
ReplaceUiSoundsstated the wrong replacement lengths. Its README andmeta.jsonquoted 888 / 456 / 914 ms — the banks of 2026-08-27, before the.mp3s were regenerated. The shipped banks actually measure 340 / 444 / 601 ms. Both length columns are now read off the real files bytests\ObjCodecTests(DemoBankTests.cs): the replacement out of the shippedDist\Sounds\<mediaId>.bnk, the shipped one out of the game's own.wem. A re-bake that changes a length now fails the test instead of leaving the table wrong.
Verified in the game
docs\PROVEN-FOUNDATIONS.md has nothing filed PENDING-INGAME any more. Closed this cycle, each with the reading behind it:
- Animation clips — the uniform grid reaching the last key (
U8-grid), a STEP curve's hold (U8-step), and a.glbwhose clips are not all bakeable or not all uniquely named still baking (U9-plan). These paths no shipped file reaches, so they shipped a real fixture:lib\u9_probe.glb, read and baked by the bake self-check, so each row has two readings that can fail apart — what the importer made of the file, and what the engine made of the clip baked from it. - The loop flag (
U12) and the chosen clip (U13) — read off a live actor'sAnimatorOverrideController: the declared clips come backisLooping=True, the undeclared onesFalse, and theAnimatordrives the"play"clip even though it is deliberately not the file's first. The idle loop was also confirmed by eye in a live mission. - The drive verdict (
U9-verdict) on its pass path, and the donor-shader path (U3b/U3c) at runtime. Q1, the quit cutscene — the discriminating log line was finally captured, so it is the cutscene's own callback that quits the game, not the watchdog deadline.- A content mod that does not declare ContentTool as a dependency comes back on a cold launch through the startup reconcile alone (
demos\NoDepTexture). GUI_StatsPlusClickis pinned: the soldier progression screen's three stat+buttons (Willpower, Strength, Speed), found live rather than guessed — the event is wired by def, not by name in code, which is why it had zero hits in the decompile.
ContentTool 1.1.0
Import and bake fixes driven by a real modding session — a modder could not get his character models into the game, and every problem he hit turned out to be ours.
Skinning quality
- Up to four bone influences per vertex. ContentTool used to keep only the two strongest weights and discard the rest. That was never a Phoenix Point or Unity requirement — the game itself ships meshes with both two and four, declared per mesh. Replacing one of its four-influence meshes actively downgraded it. A replacement now keeps whatever the target mesh declares, and a new model added from a .glb carries what the file really has. More than four is still unsupported and is refused rather than silently trimmed.
- Per-bone bounds cover every influence. Bones weighted only in the third or fourth slot used to get empty or undersized boxes, which could make body parts cull or pop during animation.
Models that used to be refused or mangled
- Body-part meshes exported with the rig now import. Each attachment joint hangs under a different bone, which is normal for that kind of export; the importer used to reject it and told you to run Object > Apply > All Transforms. That advice was wrong and could damage a rig — it is gone, along with the refusal. You do not need to apply transforms.
- Models ripped from a live scene keep their weights. The game renames attachment bones at runtime to a decorated form, so no bone matched by name and every vertex was silently welded to its nearest bone. Those names are now understood.
- A mesh with no armature is refused on a rigged character instead of being welded into something that collapses the moment it animates. Unrigged replacements for static objects, such as weapons, work exactly as before.
Baking
- One unusable file no longer kills the whole build. Any refusal — a missing texture, a bad material, an unfound mesh — used to abandon that bundle and every bundle after it, producing no output at all while reporting a single failure. Problems are now reported and skipped, and the rest still bakes.
- A failed bake can no longer leave an older bundle marked current, so you cannot unknowingly test stale content. Note the consequence: while a project still reports a failure, it re-bakes on every launch.
- Refusal messages say what is actually wrong and where — a texture in the wrong folder now names the folder it is in and the one it belongs in.
- New warning: if a source mesh contains a tiny leftover part (a stray triangle from cutting or optimising), it now tells you, because the game assigns materials by part order and a shard can push your real geometry onto the wrong material.
Workbench
- The unit picker is visible again. The list of 791 templates was being pushed off the bottom of the panel, so the workbench appeared stuck on one model. Added
ct_bench unit <name>to select one from the console.
Documentation
New guide: Replace vs Add — which one you want, why a replacement binds to the game's own skeleton, and why a Replace mod bakes once on the player's machine while an Add mod ships ready to run.
ContentTool 1.0.0
ContentTool is the content engine other Phoenix Point mods build on. A mod that depends on it can
replace what the game ships — textures, models, materials, animation curves, sounds, videos,
whole asset bundles — and add content the game never had, such as a new weapon or a new
creature. Everything is applied in memory while the game runs: your Phoenix Point installation is
never written to, and removing the mod leaves nothing behind to undo.
On its own ContentTool changes nothing. You are most likely here because a mod you installed
requires it.
Install
- Download
ContentTool-1.0.0.zipbelow. - Copy the
ContentToolfolder into<Steam library>\steamapps\common\Phoenix Point\Mods\. - Start the game, open Mods, tick Content Tool.
If a content mod declares ContentTool as a dependency, the mod manager enables it for you.
What is new in this release
A model off the internet can be a playable soldier — with no C# at all.
The HumanoidSoldier demo takes a foreign humanoid .glb, fits it onto Phoenix Point's own
skeleton, and the campaign starts you with it. The whole mod is a meta.json and a
ppcontent.json; there is no assembly, no Harmony patch, nothing to compile.
A shipped character can be given a new body. ReplaceCharacterBody swaps the body of a
character the game already ships, again from the manifest alone.
"startingRoster": true puts a code-less mod's unit in the roster the campaign begins with, so
a new soldier is playable from the first mission rather than something you have to spawn.
"useGameAnimations": true plays the game's own animation clips on your model. A mod that
sets it ships zero animation bytes — the walk, the aim, the reload all come from Phoenix Point.
This is new and not yet battle-tested: it is the fastest route to a moving character, but expect
rough edges on rigs that stray far from the shipped proportions.
The offline model pipeline — tools\ppskel.py (fit a foreign rig to the game's skeleton),
ClipCensus (sample the shipped clips), tools\ppretarget.py (retarget onto them),
tools\ppzip.py (compress). Together they cut the demo model by 65% with every clip kept.
Fixes that matter if you are building a .glb creature
- The aimed shot that never came back. A null in
AimIKon a custom-rigged creature froze the
camera mid-shot and left the turn hanging. Fixed — an aimed shot on a foreign rig now resolves. - Animation events are inherited from the vanilla clips. Without them a custom body played its
animation silently and without effect: no footstep, no muzzle flash, no hit frame. Your model now
gets the events the original clip carried.
Notes
- Sounds are the one thing that cannot be taken back mid-session. Unticking a mod leaves a replaced
sound loaded until you restart; restarting is the clean undo. - One shipped resource has exactly one owning mod. Two mods claiming the same one is refused by
name in your player log, never silently. - If a mod's content does not appear, the reason is a named line in
Player.log— search it for
ct_.
Documentation for modders: https://ubermorgott.github.io/PhoenixPoint-Mod-ContentTool/