Skip to content
k-mods.com

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: cdo
conditions:
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_C
patches:
- target: /Script/FactoryGame.FGBuildableFrackingActivator
properties:
- path: mDefaultPotentialExtractionPerMinute
op: multiply
value: 1.5

The 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 default
conditions:
- 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: true

Keys 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: OR
conditions:
- hasMod: RefinedPower
- hasMod: FicsitFarming

This 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.

gated.cdo.yml
type: cdo
conditions:
- hasMod: KAPI
- config: features.enableBonus && balance.ironSpeed >= 2
patches:
- target: /Game/FactoryGame/Recipes/Smelter/Recipe_IngotIron.Recipe_IngotIron_C
properties:
- path: mManufactoringDuration
op: multiply
value: 0.9

A 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 reload but is reported as restart advised, and the editor warns with config.hot-in-condition.
  • Hot fields are the right choice for conditions on entries inside cdo patches, 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).

modules.config.yml
type: config
category: SampleFeaturesCategory
fields:
- 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: true
pack.yml
ref: KDFSample
name: KDataForge Sample Pack
version: 1.0.0
conditions:
config: modules.enableBalance

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.

gated-ingredients.recipe.yml
type: recipe
recipes:
- 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 on

Entry 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.

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 contract
name: My Overhaul Pack
version: 2.1.0
description: A short, player-facing summary of what the pack changes. # optional; shown by settings screens
icon: icon.png # optional; pack-relative png/jpg/jpeg/tga/bmp or a /Game/... texture path
priority: 200 # higher than default (100) → applies earlier in every stage
enabled: true
conditionBehaivor: AND # optional; controls condition-object aggregation
dependencies:
- KDFSample # packs that must load before this one
conditions: # same grammar/evaluator as a document's conditions: see above
hasMod: RefinedPower@>=4.0.0

description 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 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:

pack.yml
ref: MyRefinedPowerAddon
name: My RefinedPower Addon
version: 1.0.0
conditions:
- hasMod: RefinedPower@>=4.0.0
- hasMod: LegacyRefinedPowerBridge
ifNotMatch: true

This 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.

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: MyOverhaul
name: My Overhaul Pack
version: 3.0.0
redirects:
OldItemId: NewItemId # OldId: NewId

Full worked example (rename MySuperIngot → SuperIngotMk2 across a save): End-to-End Pack → Renaming an id.

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:

overrides/late-tweaks.cdo.yml
type: cdo
include:
- 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.0

Tags 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.