Class ContentDirectoryBuilder
Builds the content directories — the art half of a scope, next to the content package's data half.
One directory for the base game, one per mod. Not one per root asset:
- A content build follows every reference from its roots and writes what it reaches, so shared dependencies are *duplicated* across directories. Sixteen scope directories would re-ship the same meshes and textures sixteen times — the measurement is already 341 MB / 3049 files for 100 prefabs, essentially all of it Polygon* packs dragged in as dependencies.
- Per-mod directories are not an optimisation but the override mechanism: registration order is search order, so a mod registered after the base game wins, and a mod can be unregistered without touching anything else. That only needs a boundary between the base game and each mod, not between scopes.
- Splitting the base game into several directories stays possible later (the plan says scopes must be re-cut with real build numbers), and it is a change here, not in the runtime.
Output goes outside Assets/: a content directory is build output, and under Assets/ Unity would import
every file into the AssetDatabase (3049 files for a 100-prefab scope) and it would end up committed. The
player gets it through StreamingAssets instead — ContentBuildPreprocessor injects it, and on Web it is
preloaded into the VFS, both of which are how the runtime finds it at the path
BaseDirectory expects.
Build target: a content directory is built for the active build target and fails per file in any other player ("File's Build target is: 19"). The caller must have switched the active target before calling.
public static class ContentDirectoryBuilder
- Inheritance
-
objectContentDirectoryBuilder
Fields
- BaseName
The registration name of the base game's directory.
Properties
- BaseOutput
The base game's output folder for one build target. Its final segment is the registration name, so it must be stable; the target segment above it must not be, because a content directory is only valid for the target it was built for.
Target-specific output on purpose. One shared folder looked harmless and was not: the editor play session reads the same folder (BaseDirectory) and the editor runs on Windows whatever the active target is, so building content for WebGL left the editor with a directory it cannot load — every read failing per file with "File's Build target is: 19", which reads like a content bug and is really a path bug. Two targets, two folders, no interference.
- DetachedResultPath
Where a detached build writes its one-line report, and the file that says one is running.
- EditorBaseOutput
The editor's own output folder. The editor plays on the host platform, not the active target.
- OutputRoot
Where build output lives: outside
Assets/, inside the git-ignoredBuild/.
- RunningMarkerPath
Present while a detached build runs, so "busy building" can be told from "stuck" without the bridge.
- StampPath
Where the input fingerprint of the current output is recorded. A sibling of the output folder, because the output folder itself is cleared at the start of every build.
Methods
- Build(string, string, string[], bool)
Builds one directory. Returns a one-line report; never throws at the caller.
- BuildBase(bool)
Builds the base game's content directory from every root under GrindFest.Content.Editor.ContentDirectoryBuilder.RootsFolder, unless the existing output was already built from exactly this input. Safe to call from a build preprocessor, which is what turns "the game has content" into "the player ships it" — and it is called there on every player build, which is why it has to be able to answer "already done" in milliseconds instead of spending twelve minutes re-serialising 18 900 files nothing has touched.
- BuildBaseDetached(bool, string)
Starts BuildBase(bool) on the next editor tick and returns at once, writing the report to a file when it finishes.
Why this exists: the build runs on the main thread for ~12 minutes, and every synchronous way of asking for it is a twelve-minute block. The editor looks frozen, and anything that talks to the editor over a pipe — the agent bridge, a CI step, a menu click — sits on that pipe for the whole build with Unity perfectly healthy and nothing in either log to say so. Measured 19.09.2026 while tuning this: an agent call waited out the build and had to have its client restarted, twice, because the wait is indistinguishable from a hang. Detaching makes the wait the caller's choice: poll RunningMarkerPath, then read DetachedResultPath.
- Fingerprint(string[])
The fingerprint of everything a content build's output depends on: the root assets, by the AssetDatabase's own dependency hash, plus the root set itself.
One call per root, no walking — the roots are the build's input, and every prefab, material and mesh the build will write is reached from one of them. A change anywhere in that reach changes the hash of the root that reaches it, which is exactly the question "would this build produce something different".
- FirstError(BuildReport)
The first error the build reported, so a failure names itself instead of only counting.
This is how
Gather and collect build metadata: Unexpected header for AssetBuildMetaData blob from VirtualArtifacts/Extra/<hash>.buildmeta— the message that identifies unbuildable authoring data — gets out of a report nobody reads.
- ModOutput(string)
A mod's output folder, named after the mod so the registration name is unique per mod.