Conditions & Packs
Every condition form for gating whether a document applies, pack manifest options, id redirects, and include: ordering between files in the same pack.
Two mechanisms sit above the individual document types covered elsewhere in this section. The
conditions: block decides whether a single document applies at all. The pack.yml manifest
describes the folder of documents around it: a stable ref for save persistence, a load-order
priority, and optional dependencies on other packs. Both are cross-cutting, so they work the same
way no matter which document type: (cdo, item, gametag, …) they’re attached to.
Conditions: gating whether a document applies at all
Section titled “Conditions: gating whether a document applies at all”A conditions: block lets a document skip itself when the environment doesn’t match: a required
mod isn’t loaded, the game version is too old, or a class from another mod doesn’t exist. It accepts
one object or an array of objects. Keys inside each object are AND-combined; conditionBehaivor
selects AND or OR across objects and defaults to AND. Ranges use SML’s semver syntax (>=,
^, ~, exact versions, || unions). The example below exercises every supported key, including
both the scalar and sequence forms of hasMod/hasClass:
type: cdoconditions: gameVersion: '>=1.0.0' # semver range vs FactoryGame version
hasMod: KAPI # single mod (loaded check) # hasMod: # sequence ALL must be loaded # - KAPI # - RefinedPower@>=4.0.0 # inline version constraint
modVersion: # explicit version map form SML: '>=3.12.0' KAPI: '>=2026.0.0'
hasClass: /Script/FactoryGame.FGBuildableFrackingActivator # hasClass: # sequence ALL must exist # - /Game/FicsitFarming/Buildable/FarmPlot/Build_FarmPlot.Build_FarmPlot_Cpatches: - target: /Script/FactoryGame.FGBuildableFrackingActivator properties: - path: mDefaultPotentialExtractionPerMinute op: multiply value: 1.5The map form above remains the compact form for ordinary positive conditions. Use an array of condition objects when some requirements must match and another must not match:
conditionBehaivor: AND # optional; AND is the defaultconditions: - hasMod: - KAPI - KLib # both mods must be loaded - hasMod: IncompatibleMod # this object must NOT match ifNotMatch: true - hasMod: 'OptionalIntegration@^2.0.0' # reject only matching 2.x versions ifNotMatch: trueKeys inside each object are AND-combined, then ifNotMatch: true negates that object’s combined
result. conditionBehaivor combines the resulting objects and accepts AND or OR
(case-insensitive); it defaults to AND. Correctly spelled conditionBehavior is accepted as an
alias. Therefore, the example applies only when KAPI and KLib are loaded, IncompatibleMod is
absent, and OptionalIntegration is either absent or outside ^2.0.0.
Inversion applies to the whole object, not each list entry. To reject several mods independently,
put each hasMod check in its own object with ifNotMatch: true. Invalid keys, value types, or
version ranges are configuration errors and are never converted into matches by inversion.
Use OR when any one condition object may enable the document:
conditionBehaivor: ORconditions: - hasMod: RefinedPower - hasMod: FicsitFarmingThis applies when RefinedPower, FicsitFarming, or both are loaded. ifNotMatch still runs per object
before OR combines the results. Invalid behavior values or malformed condition objects always fail
closed, even when another OR object matches. With an empty condition array, AND passes and OR
does not.
Failing conditions skip the document with an Info-severity diagnostic. However, /kdf report
filters Info diagnostics out of its printed output entirely, so a condition skip is currently
not visible there. The only signal is a lower-than-expected “document(s) applied” count. See
Debugging a Pack That Won’t Apply
for the workaround.
Conditions that read pack configuration: config:
Section titled “Conditions that read pack configuration: config:”config: is one more key of a condition object. Its value is an
expression over the fields of your
pack configuration and over game settings, or a list of
expressions that must all be true. It combines with hasMod, gameVersion, modVersion,
hasClass, ifNotMatch and conditionBehaivor like any other key.
type: cdoconditions: - hasMod: KAPI - config: features.enableBonus && balance.ironSpeed >= 2patches: - target: /Game/FactoryGame/Recipes/Smelter/Recipe_IngotIron.Recipe_IngotIron_C properties: - path: mManufactoringDuration op: multiply value: 0.9A config: condition works in pack.yml, in a document’s conditions:, and on single entries of
lists (next section). It does not work inside a config definition file. Two practical rules:
- Conditions on a whole pack, or on documents that generate content (
item,recipe,schematic, …), run at load time. Use a cold field there. A hot field works through/kdf reloadbut is reported as restart advised, and the editor warns withconfig.hot-in-condition. - Hot fields are the right choice for conditions on entries inside
cdopatches, data assets and operation lists, which are re-applied on every reload.
A pack that its own config: condition switched off still lists its settings (marked as not
active), so a player can switch it back on. session: and ags: settings do not exist on the main
menu; a config: condition that reads one is not an error there, the pack or document is deferred
until a save is loaded (ifNotMatch: true does not turn such an unknown value into a match).
type: configcategory: SampleFeaturesCategoryfields: - id: modules.enableBalance type: boolean name: Enable the balance module description: Turns the rebalancing documents of this pack on or off. Needs a restart. default: trueref: KDFSamplename: KDataForge Sample Packversion: 1.0.0conditions: config: modules.enableBalanceConditions on single list entries
Section titled “Conditions on single list entries”The keys conditions, conditionBehavior and conditionBehaivor are allowed on any map entry of
any list, at any depth: operations, patches, ingredients, unlocks, nodes, members of a value
payload, and so on. Before the document is applied, an entry whose condition is false is removed;
for an entry whose condition is true, the condition keys are stripped. An entry condition that fails
to evaluate removes the entry and logs an error.
type: reciperecipes: - id: Recipe_GatedPlate parent: /Game/FactoryGame/Recipes/Constructor/Recipe_IronPlate.Recipe_IronPlate_C properties: - path: mIngredients value: - ItemClass: /Game/FactoryGame/Resource/Parts/IronIngot/Desc_IronIngot.Desc_IronIngot_C Amount: 3 - ItemClass: /Game/FactoryGame/Resource/Parts/Screw/Desc_Screw.Desc_Screw_C Amount: 2 conditions: config: features.enableBonus # only present while the bonus is onEntry conditions accept the same keys as document conditions. Limits: they are not looked for
inside conditions: blocks, an entry-level hasClass cannot see classes that KDataForge itself
generates, and operands of where: filters resolve cfg: references but not apply-time templates.
pack.yml: the pack manifest
Section titled “pack.yml: the pack manifest”A pack.yml at a detected pack root turns it into a named pack instead of the loader’s fallback.
The implicit ref and name come from the detected root’s parent directory. The manifest gives a
group of documents a stable identity across saves, a load-order priority relative to other packs,
and dependency ordering. The fields below are read directly by the pack scanner:
ref: MyOverhaul # stable ref the save-persistence and conflict-report contractname: My Overhaul Packversion: 2.1.0description: A short, player-facing summary of what the pack changes. # optional; shown by settings screensicon: icon.png # optional; pack-relative png/jpg/jpeg/tga/bmp or a /Game/... texture pathpriority: 200 # higher than default (100) → applies earlier in every stageenabled: trueconditionBehaivor: AND # optional; controls condition-object aggregationdependencies: - KDFSample # packs that must load before this oneconditions: # same grammar/evaluator as a document's conditions: see above hasMod: RefinedPower@>=4.0.0description is a string or a localized {key, ns, source} map. icon is a file inside the pack
folder (png, jpg, jpeg, tga or bmp; a path that leaves the folder is rejected) or an object
path of a UTexture2D that starts with /. Both are shown by pack settings screens, see
Pack Configuration. The
editor reports a missing icon file as pack.icon-missing.
dependencies: are hard ordering constraints. A pack is rejected if a dependency is missing,
self-referential, or already invalid; that rejection propagates to transitive dependants. Packs in
a dependency cycle are also rejected. The remaining packs are topologically ordered so every
dependency runs first. Among packs whose dependencies are ready, priority descending is the
tie-break, followed by ref ascending. priority defaults to 100.
pack.yml conditions: gating a whole pack
Section titled “pack.yml conditions: gating a whole pack”pack.yml accepts the exact same conditions: map-or-array and evaluator as a document. Every key
from Conditions: gating whether a document applies at all
above (gameVersion, hasMod, modVersion, hasClass) plus condition arrays, ifNotMatch, and
conditionBehaivor works here unchanged. It’s evaluated once, before any file in the pack is even
read. A failing pack is skipped entirely with a single Info-severity diagnostic naming the pack,
instead of every document in it failing its own copy of the same condition:
ref: MyRefinedPowerAddonname: My RefinedPower Addonversion: 1.0.0conditions: - hasMod: RefinedPower@>=4.0.0 - hasMod: LegacyRefinedPowerBridge ifNotMatch: trueThis whole pack loads only when RefinedPower 4.0+ is present and LegacyRefinedPowerBridge is not
loaded. Use pack conditions for “this whole pack is a compat/addon layer for mod X” instead of
repeating a conditions: block with hasMod: X at the top of every document. A document can still carry its
own conditions: on top of the pack’s. Pack and document gates must both pass; each gate uses its
own conditionBehaivor value.
redirects: renaming generated-content ids
Section titled “redirects: renaming generated-content ids”Generated class names are id-based (Gen_<PackRef>_<Id>), so renaming an id would orphan save
data. A redirects: entry in pack.yml feeds FCoreRedirects so old class paths in existing
saves resolve to the renamed class. Renaming a file never needs a redirect. Only id renames do:
ref: MyOverhaulname: My Overhaul Packversion: 3.0.0redirects: OldItemId: NewItemId # OldId: NewIdFull worked example (rename MySuperIngot → SuperIngotMk2 across a save):
End-to-End Pack → Renaming an id.
include: file ordering inside a pack
Section titled “include: file ordering inside a pack”Documents in the same stage use deterministic relative-path order unless include: adds an
ordering constraint. List another file’s path under include: (relative to the pack root) when
this document must apply after it, even though both stay in the same stage:
type: cdoinclude: - base/base-values.cdo.yml # relative to the pack root; applies first (same stage)patches: - target: /Game/FactoryGame/Recipes/Smelter/Recipe_IngotIron.Recipe_IngotIron_C properties: - path: mManufactoringDuration op: multiply # multiplies the value base-values.cdo.yml already set value: 2.0Tags registered elsewhere in the same pack
Section titled “Tags registered elsewhere in the same pack”A gametag document doesn’t need include: ordering against the documents that use its tags. The
GameTags stage always runs before every other stage, so a tag registered anywhere in a pack is
already usable via matchTag: or tag-container ops in that same pack’s cdo documents. The full
syntax for registering tags, how registration survives the engine’s tag-tree rebuilds, and worked
examples of tagging content and then bulk-patching by tag all live on their own page:
Type: gametag.
