Skip to content
k-mods.com

Player-Configurable Balancing

Turn fixed pack numbers into settings players can change. Config fields, typed references, templates, config conditions, hot and cold values, and testing in single player and multiplayer.

In this guide you give a pack five settings: how fast iron smelts, whether the output is doubled, which tier a milestone appears in, which tags a recipe carries, and a line of description text. You will see hot values change live and a cold value ask for a restart. The files are the configuration part of the KDFSample sample pack, so you can compare them with that pack.

Read Pack Configuration first if you want the full rules behind each step. This page is the walkthrough.

FieldTypeApplyUsed for
balance.ironSpeedfloat slider 0.25 to 4hotDivides the Iron Ingot smelting time
features.enableBonusbooleanhotSwitches the doubled output on and off
features.tierselect (int32, 1 to 3)coldHUB tier of the Super Ingot milestone
tags.extralist of gameplay tagshotTags added to the Iron Ingot recipe
texts.mottomultiline texthotFirst sentence of the Super Ingot description

Create DataForge/pack.yml. description and icon are shown next to the pack in settings screens; the icon is a file in the pack folder.

pack.yml
ref: KDFSample
name: KDataForge Sample Pack
version: 1.0.0
description: Exercises pack configuration, templates and config conditions.
icon: mod-logo.png

A settings screen groups fields by category and orders the groups by priority. Categories are ordinary generated classes whose parent is FGCategory. Create DataForge/sample-config-category.class.yml:

sample-config-category.class.yml
type: class
classes:
- id: SampleBalanceCategory
parent: /Script/FactoryGame.FGCategory
properties:
- path: mDisplayName
value:
key: SampleBalanceCategory_Name
ns: KDFSample
source: Sample Balance
- path: mMenuPriority
value: 10
- id: SampleFeaturesCategory
parent: /Script/FactoryGame.FGCategory
properties:
- path: mDisplayName
value:
key: SampleFeaturesCategory_Name
ns: KDFSample
source: Sample Features
- path: mMenuPriority
value: 20

Create DataForge/kdf-sample.config.yml. The file name ending in .config.yml is what makes it a definition. category: at the top is the default; balance.ironSpeed overrides it.

kdf-sample.config.yml
type: config
category: SampleFeaturesCategory
fields:
- id: balance.ironSpeed
type: number
valueType: float
name:
key: Config_IronSpeed_Name
ns: KDFSample
source: Iron smelting speed
description: How many times faster the Iron Ingot recipe runs than in the base game.
tooltip:
title: Iron smelting speed
text: 1 keeps the vanilla speed, 2 halves the smelting time, 4 is the maximum.
category: SampleBalanceCategory
default: 2
min: 0.25
max: 4
step: 0.25
decimals: 2
display: slider
apply: hot
- id: features.enableBonus
type: boolean
name: Double iron ingot output
description: Doubles the amount of Iron Ingots one smelting cycle produces.
default: true
apply: hot
- id: features.tier
type: select
valueType: int32
name: Super Ingot milestone tier
description: HUB tier in which the Super Ingot Production milestone appears.
default: 1
options:
- value: 1
label: Tier 1
- value: 2
label: Tier 2
- value: 3
label: Tier 3
apply: cold
- id: tags.extra
type: text
valueType: tag
multiple: true
tagRoot: KDF.Sample
name: Extra recipe tags
description: Gameplay tags that are added to the Iron Ingot recipe.
default:
- KDF.Sample.Test
maxCount: 4
apply: hot
- id: texts.motto
type: text
valueType: text
multiline: true
maxLength: 200
name: Super Ingot motto
description: The first sentence of the Super Ingot description.
default: Forged from YAML. Stacks high, sinks well.
apply: hot

Every field has a name, a description and (through the field or the document) a category. Start the game once. KDataForge creates FactoryGame/Configs/Packs/KDFSample.config.yml with the defaults.

Typed references replace a whole, unquoted scalar with the field’s typed value. Create DataForge/iron-ingot-faster.cdo.yml:

iron-ingot-faster.cdo.yml
type: cdo
conditions:
gameVersion: '>=1.0.0'
patches:
- target: /Game/FactoryGame/Recipes/Smelter/Recipe_IngotIron.Recipe_IngotIron_C
properties:
# 1. Typed reference: the number 2.0 by default
- path: mManufactoringDuration
op: divide
value: cfg:balance.ironSpeed
# 2. Element condition: this operation exists only while the bonus is on
- path: mProduct[0].Amount
op: multiply
value: 2
conditions:
config: 'features.enableBonus'
# 3. A list field becomes a list of tags
- path: mGameplayTags
op: append
value: cfg:tags.extra

The three operations use all three ways to read a value:

  • cfg:balance.ironSpeed fills the value: with the number. Because the slider’s minimum is 0.25, the division never hits zero.
  • conditions: { config: ... } on the second operation takes the operation out of the list while features.enableBonus is off. It is an element condition and works on any entry of any list.
  • cfg:tags.extra is a list field, so the reference becomes a list.

Templates put values into strings. A string that starts with { has to be quoted. Create DataForge/super-ingot.item.yml:

super-ingot.item.yml
type: item
items:
- id: SuperIngot
parent: /Game/FactoryGame/Resource/Parts/IronIngot/Desc_IronIngot.Desc_IronIngot_C
properties:
- path: mDisplayName
value:
key: SuperIngot_Name
ns: KDFSample
source: Super Ingot
- path: mDescription
value: '{cfg:texts.motto} Smelts like {item:/Game/FactoryGame/Resource/Parts/IronIngot/Desc_IronIngot.Desc_IronIngot_C:GetItemName} at {cfg:balance.ironSpeed}x speed.'
- path: mStackSize
value: SS_HUGE

With the defaults the description reads: Forged from YAML. Stacks high, sinks well. Smelts like Iron Ingot at 2.0x speed.

  • {cfg:texts.motto} and {cfg:balance.ironSpeed} are replaced before anything is applied.
  • {item:<class>:GetItemName} is read from the game when the property is written: the item provider calls the engine function GetItemName on the Iron Ingot class and returns the localized name. See Templates.

The milestone’s tier is read when the game registers the schematic, so it is a cold value. In the schematic document the tier property takes the field like any other value:

properties:
- path: mTechTier
value: cfg:features.tier

features.tier has apply: cold, which is also the default. Changing it in the values file changes the saved value immediately, but the running game keeps the tier it started with and reports restart required.

Load a save and open the chat.

/kdf config dump KDFSample

The fields are listed in no particular order, one line each, with the value, the default and the effective value:

KDFSample:balance.ironSpeed [hot] = 2.0 (default 2.0, effective 2.0)
KDFSample:features.enableBonus [hot] = true (default true, effective true)
KDFSample:features.tier [cold] = 1 (default 1, effective 1)
KDFSample:tags.extra [hot] = [KDF.Sample.Test] (default [KDF.Sample.Test], effective [KDF.Sample.Test])
KDFSample:texts.motto [hot] = Forged from YAML. Stacks high, sinks well. (default Forged from YAML. Stacks high, sinks well., effective Forged from YAML. Stacks high, sinks well.)
5 configuration field(s)

Change a hot value and apply it:

/kdf config set KDFSample:balance.ironSpeed 3
/kdf config apply KDFSample

The reply says Saved: yes, reloaded: yes and lists the changed key. The Iron Ingot recipe now takes a third of its original time, and the Super Ingot description shows 3.0x. Turn the bonus off the same way (/kdf config set KDFSample:features.enableBonus false) and the recipe’s output returns to normal after the apply, because the element condition removed the operation.

Now the cold value:

/kdf config set KDFSample:features.tier 2
/kdf config apply KDFSample

The reply ends with Restart required: KDFSample:features.tier (in yellow). The file now says tier: 2, and /kdf config dump shows the value 2, restart required, and an effective value that is still 1. After the next start the milestone sits in tier 2.

You can also edit FactoryGame/Configs/Packs/KDFSample.config.yml with a text editor and run /kdf reload: hot values apply, cold values report restart required. To try templates and expressions without touching any file:

KDF.Template "{cfg:texts.motto} x{cfg:balance.ironSpeed}" KDFSample
KDF.Config.Expr "features.enableBonus && balance.ironSpeed >= 2" KDFSample

Open the project in the desktop editor, or run kdf lint on the folder that contains DataForge/. The editor checks the definition (required name and description, ids, ranges, defaults, categories), every cfg: reference and config: expression, template syntax, and whether a hot value feeds a cold place. Try breaking something:

EditDiagnostic
cfg:balance.ironSpedconfig.unknown-id
config: !features.enableBonusconfig.expression-quote
value: {cfg:texts.motto} texttemplate.unquoted-start
remove description from a fieldschema.required
icon: nope.pngpack.icon-missing

See Lint Your Pack and the desktop editor.

Players can use the chat commands above, or edit the file. For a real settings screen, build a widget with the Blueprint Config API: list the packs with Get All Pack Infos, the fields with Get Config Descriptions Flat, create a slider, a checkbox, a dropdown and a text box per Get Kind, and call Apply Config Changes from an Apply button. The reference describes the full node flow.

  • On a listen server, the host changes values and they are sent to the clients while connected. Clients cannot edit them.
  • On a dedicated server, edit the server’s values file and run /kdf reload, or change values as an administrator from a settings screen or with /kdf config set and /kdf config apply.
  • A client whose cold features.tier differs from the host’s keeps its loaded content, reports a host mismatch, and can adopt the host’s values from the UI.
  • A client never writes the host’s values to its own file, and leaving the session restores the local values.

All players still need the same packs installed. See Pack Configuration.

SymptomFix
/kdf config dump lists 0 fieldsThe *.config.yml file is outside the pack, or a field has an error. Run /kdf report.
The recipe did not change after applyThe field is cold (restart), or the patch target is wrong. Check apply: hot and /kdf report.
The description still shows {cfg:texts.motto}The scalar was not quoted, or the id is misspelled. A pre-pass token that cannot be resolved stays as written and logs a warning.
Restart advised after applying a hot valueThe hot field also feeds a cold place. Use a cold field there, or restart.
The category shows as FGCategoryThe category class was not found. Check the id in category: and that the .class.yml is in the same pack.