Debugging a Pack That Won't Apply
An ordered, top-to-bottom checklist for "my YAML file doesn't seem to be doing anything." Six checkpoints, from file discovery to session initialization.
Every other page in Features has its own scattered “Troubleshooting” section for symptoms specific to that document type. This page is different. It’s the one ordered sequence you follow top to bottom the moment something you wrote just isn’t happening and you don’t yet know why. Each step below rules out exactly one stage of the pipeline before you move to the next, so don’t skip ahead.
The six steps mirror the real pipeline, in the real order it runs
(Source/KDataForge/Private/Subsystems/KDFSubsystem.cpp): scan → parse/type-detect → conditions →
validate → apply → restart scope when testing YAML changes.
Step 1: is the file even being scanned?
Section titled “Step 1: is the file even being scanned?”KDataForge starts at the Unreal project root, which is FactoryGame/ in a packaged game, and walks
up to 6 levels deep. A directory becomes a pack root when it is named DataForge, uses the
legacy name data-editor, or directly contains pack.yml. Names are case-insensitive. While
walking, the scanner prunes Content, Saved, Intermediate, Binaries, DerivedDataCache,
Plugins, Source, and Config, so a root nested inside one of those directories is never found.
Everything below a detected root is scanned for *.yml and *.yaml. The one exception is
pack.yml, which is read separately as the manifest. Roots may nest; each file belongs to its
deepest enclosing root, never both the child and parent pack.
Run /kdf report. The first line is always:
KDataForge: N pack(s), N document(s) applied, N op(s), N actor patch(es), N error(s), N warning(s)- If the pack count didn’t change after adding your folder, it wasn’t found. Check the exact
root marker (
DataForge,data-editor, or a directpack.yml), its depth from the project root, and that no parent directory in the path matches the pruned list above. - A pack directory that resolves but has
enabled: falseinpack.ymlis excluded from the pack count entirely. It’s never added to the pack list, and it produces only an Info-level “Pack ’…’ is disabled, skipped” diagnostic. See Step 3 for why that won’t show up in the report text either. A disabled pack therefore looks identical, from the report’s perspective, to a pack that was never found at all. - Two packs resolving to the same
ref: the second directory is skipped entirely with a Warning, “Duplicate pack ref ’…’, directory skipped.” This is visible in/kdf report.
If your pack doesn’t even move the pack/document counts, nothing past this point matters yet. Fix the location first.
Step 2: did the root type resolve?
Section titled “Step 2: did the root type resolve?”First confirm the file parsed. Malformed YAML is an Error; that file is skipped, while every
other file continues through the load. Fix the YAML parse failed diagnostic before checking its
root type.
A document’s type comes from either an explicit type: key or the <name>.<type>.yml filename
convention (e.g. super-ingot.item.yml implies type: item). There are two distinct ways this can
go wrong, and they look different in the report.
No type could be determined at all happens when there’s no type: key and the filename doesn’t
match the convention either. This produces one Warning per offending file, from ScanAndParse:
[Warning] my-pack/mystery-item.yml: No 'type:' key and no '<name>.<type>.yml' filename convention document skippedA type was determined, but no handler owns it happens with a typo like type: reciept, a
<name>.reciept.yml filename, or a type that belongs to a mod you don’t have loaded. This warns
only once per distinct type string across the entire load, not once per file:
[Warning] my-pack/typo.reciept.yml: No handler registered for document type 'reciept' (planned for a later phase?)That dedup matters. If you copy-pasted the same typo into five files, you’ll only see one warning line, not five, so don’t assume only the reported file is affected.
Both lines follow FKDFDiagnostic::ToString()’s exact contract: [Severity] file: message, or
[Severity] file:line: message when a line number is known. Most scan/type diagnostics don’t carry
one, since they’re about the file as a whole, not one YAML node.
Step 3: did conditions pass?
Section titled “Step 3: did conditions pass?”A conditions: object or array (gameVersion, hasMod, modVersion, hasClass, per-object
ifNotMatch, and top-level conditionBehaivor: AND|OR, see
Conditions & Packs → Conditions)
is evaluated once per document, before validation or apply. A failing condition produces exactly
one diagnostic:
Skipped by conditions: Required mod 'SomeMod' is not loadedThis diagnostic is Info severity, and BuildReportString (the text behind /kdf report)
explicitly skips every Info-severity entry before printing. That’s not a report-formatting quirk
you can work around with a flag. The same filtered string is also what the in-game editor’s
load report scans for [Error]/[Warning] to decide whether to flag a problem, so a
condition typo produces zero visible signal anywhere a player normally looks. The document is
silently skipped, every single time, and neither /kdf report nor the in-game editor will ever
mention it.
There is currently no player-facing switch that surfaces Info diagnostics: no debug-log line, no
/kdf report -v, nothing. The condition evaluator itself doesn’t log anything either. It only ever
returns a fail reason string that the framework then buries at Info severity.
The only reliable way to catch this today is to temporarily delete or comment out the document’s
conditions: block and restart the session. If your document suddenly counts toward “document(s)
applied” (or your op’s debug line from Step 5 appears) with the
block removed, the condition was the culprit. Put the block back and check it against
Conditions & Packs for exact key names, mod ref
spelling, version-range syntax (gameVersion/hasMod/modVersion all use SML semver ranges), and
array grouping (ifNotMatch negates only its containing object), and conditionBehaivor spelling/
value (AND is default; conditionBehavior is accepted as an alias).
Step 4: did the document validate?
Section titled “Step 4: did the document validate?”Every handler runs ValidateDocument before ApplyDocument. The two check fundamentally different
things.
-
ValidateDocumentis structural. Is the document even shaped correctly? Missing required keys, wrong node kind (map vs. sequence), or malformed op entries would all fail here. This runs purely against the parsed YAML, so it never touches the live game and can’t fail because a class doesn’t exist. Failures here areError-severity and often carry a real line number (Entry->Line), because they’re about one specific YAML node. Example, fromtype: cdo:[Error] my-pack/bad.cdo.yml:12: 'matchTag', 'matchName' and 'where' require an 'ofClass' scope class (only the recipe shortcuts 'producedIn', 'ingredient', 'product' default to FGRecipe)If
ValidateDocumentreturnsfalse,ApplyDocumentis never called for that document. It’s zero ops, and the document doesn’t count toward “document(s) applied” in the report at all. -
ApplyDocumentis semantic. Now it resolves your YAML against the actual running game. Doestarget:point at a real, loadable class or object? DoesmatchTag/producedIn/where+ofClassactually match anything? This is where a structurally-perfect document can still fail because a path is wrong or a class doesn’t exist:[Error] my-pack/bad.cdo.yml: Target '/Game/Typo/Path.Path_C' is neither a loadable class nor object (...)Critically, these failures are per patch entry, not per document. One bad
target:inside a document with several patch entries only skips that entry. The rest still apply, and the document still counts as applied as long as at least one entry succeeded.
So an Error with a line number pointing at your YAML structure means Step 4a: fix the shape. An
Error naming an unresolved path/class means Step 4b: fix the reference, or check spelling and the
_C suffix (see
Targeting → Choosing a selector).
Step 5: did the specific op apply?
Section titled “Step 5: did the specific op apply?”If you’ve gotten this far (the file is found, the type resolved, conditions passed, and the document validated and applied with no errors) but the specific value you expected to change still looks vanilla, turn on debug logging:
- Per document:
debug: trueat the top level of the file. - Per pack:
debug: trueinpack.yml. - Global: console variable
KDF.Debug 1(or launch with-KDFDebug), or/kdf debug onfrom chat. This also logs ops originating from the in-game editor.
Every successfully applied op logs exactly one line at Log verbosity from
FKDFPatchUtil::LogAppliedOp:
[KDF DEBUG] <file>: <target>.<property> <op> : <pre> -> <post>For example:
[KDF DEBUG] faster-smelting.cdo.yml: /Game/FactoryGame/Recipes/Smelter/Recipe_IngotIron.Recipe_IngotIron_C.mManufactoringDuration multiply : 2.000000 -> 1.000000There are two outcomes here.
- Your op’s line never appears at all: it never reached the apply step. That means one of
Steps 1 to 4 above is still the real problem. Most often the property path silently resolved to a
different object than you expected, or the op failed and produced an
Erroryou missed in/kdf report. A failed op logs nothing here; it only ever logs on success. - Your op’s line appears, but the pre/post values aren’t what you assumed: the pipeline worked
exactly as written. Your assumption about which property holds the value (or its starting value)
was wrong. Compare
<pre>against what you expected the vanilla value to be before writing more YAML.
The log lives at %LOCALAPPDATA%\FactoryGame\Saved\Logs\FactoryGame.log in a packaged game. Search
for [KDF DEBUG].
Step 6: did you reload or restart the session?
Section titled “Step 6: did you reload or restart the session?”Save the file, run /kdf reload as host/server administrator, then inspect /kdf report. Reload
restores reflected property baselines before re-applying every current file, so additive operations
do not stack. Changes to frozen content registrations and other restart-bound state still require a
game or dedicated-server session restart; see Smart reload.
Quick reference
Section titled “Quick reference”| Step | Question | What to check |
|---|---|---|
| 1: Scanning | Did the pack/document count in /kdf report change at all? | Root marker (DataForge, data-editor, or direct pack.yml), depth ≤ 6 from the project root, not nested inside a pruned directory, file extension .yml/.yaml |
| 2: Type resolution | Is there a [Warning] about no type / no handler for the type? | type: key spelling, <name>.<type>.yml filename convention, typo in a known type name (remember: the “no handler” warning is deduped to one per type, not one per file) |
| 3: Conditions | Does the document have a conditions: block at all? | Comment it out and reload. If the document suddenly applies, the condition (not the report) was hiding the skip. /kdf report never shows condition-skips, they’re Info-severity and filtered out |
| 4: Validation/Apply | Is there an [Error] in /kdf report, with or without a line number? | Line number present means structural (ValidateDocument), fix the YAML shape. No line number, naming a path/class, means semantic (ApplyDocument), fix the reference |
| 5: Op-level apply | Turn on debug: true / KDF.Debug 1. Does your op’s [KDF DEBUG] line appear? | Missing entirely means it never reached apply, back to Steps 1 to 4. Present but wrong pre/post means a property/assumption issue, not a pipeline bug |
| 6: Reload boundary | Did you run /kdf reload, and does this feature need a restart? | Property patches reload live; frozen registrations and irreversible world changes follow the documented restart boundaries |
Getting Started → Debugging covers all three debug switches and the log location. Its report severity table defines the Info, Warning, and Error levels used here. Diagnostics, never failures explains why a skipped patch entry never halts the rest of the load.
