Skip to content
k-mods.com

Architecture

System design, data flow, subsystems, savegame persistence strategy, and current multiplayer boundaries.

KDataForge is a data-driven runtime content editing framework. Users, and other mods, describe changes to game content in YAML documents. The framework parses these documents once at startup, validates them, and applies them through Unreal’s reflection system in a fixed, deterministic stage order.

Authors use <your-mod>/DataForge/**/*.yml. The scanner itself starts at the Unreal project root, which is FactoryGame/ in a packaged game. It recognizes directories named DataForge, the legacy name data-editor, and any directory containing pack.yml. Roots may nest, and each YAML file is assigned only to its deepest enclosing root. An implicit pack ref and name come from the detected root’s parent directory, so an explicit stable ref is recommended.

KDataForge runtime architecture from pack discovery and pack configuration through staged reflection operations and per-world actor patches

Pack configuration is the first thing a load reads: after the scan, the *.config.yml definitions and values files of every pack are loaded into UKDFConfigRegistry, before pack conditions are evaluated and before any document is parsed. The template pre-pass then replaces cfg: / session: / ags: / option: values and evaluates entry-level conditions, so the stages only ever see plain documents. See Pack Configuration Flow.

KDataForge hooks SML’s module lifecycle via two auto-discovered root modules, UKDFGameInstanceModule and UKDFWorldModule.

UKDFGameInstanceModule acts across three phases. During CONSTRUCTION, its built-in handlers exist (the subsystem’s Initialize), and this is the window in which third-party mods register their own handlers. During INITIALIZATION, UKDFSubsystem::RunInitialLoad() performs the full scan, parse, and apply pass. This runs before any world or save loads, so CDO edits and generated classes exist before anything references them. The full pass runs only once per GameInstance. During POST_INITIALIZATION, the load report is logged.

UKDFWorldModule runs per world. It registers the /kdf chat command and the configuration sync subsystem. During world INITIALIZATION, after save deserialization and before actor BeginPlay, it first tells UKDFSubsystem that the world is ready (which reloads once if a document was deferred for want of session settings), then drains queued content registration, applies schematic/research registry removals, then asks UKDFContentRemoveSubsystem to sweep save-restored recipes. Registration runs first, so an explicit removal wins when both name the same class.

SML guarantees all modules receive CONSTRUCTION before any receives INITIALIZATION, which makes the third-party handler registration window safe.

ComponentTypeResponsibility
UKDFSubsystemGameInstanceSubsystemOwns loader, handler registry, class/object caches, patch records, registrations, removals, and world rules
FKDFPackScannerstaticFilesystem discovery (pruned recursive walk, pack.yml manifests)
FKDFYamlParserstatic (KDataForgeYaml)ryml façade, the only YAML entry point; thread-safe parse, deterministic hand-written emitter
FKDFConditionEvaluatorstaticDocument, pack and entry conditions: evaluation, including config: expressions
UKDFConfigRegistrymember of subsystemOwns every UKDFConfigDescription (key <pack>:<id>), the values files, and the value lifecycle
UKDFConfigDescriptionUObject familyOne configured field; subclasses per widget kind. Public Blueprint surface (UKDFConfigBlueprintLibrary)
FKDFTemplateEnginestatic{prefix:body} templates, typed references, and the config: expression parser
IKDFTemplateProviderinterface (KDataForgeApi)Extension point that resolves one template prefix; built-ins cover cfg, session, ags, option and reflection targets
FKDFColdPropertyCatalogmember of subsystemClassifies properties as hot, cold or newActors for restart advice
FKDFReloadSchedulermember of subsystemDebounces automatic reloads caused by setting or host value changes
AKDFConfigSyncSubsystemAModSubsystemReplicates the host’s effective configuration values to clients
FKDFPropertyResolverstaticProperty path → (FProperty, value ptr) resolution incl. containers and object dereference
FKDFValueCodecstaticBidirectional YAML ↔ FProperty conversion
FKDFOpEnginestaticThe 22 property operations with per-kind capability matrix
FKDFVanillaCachemember of subsystemFirst-write snapshots for editor diff, undo, and changes-only export
FKDFDynamicContentRegistrymember of subsystemCreates deterministic generated classes/assets and reconstructs missing generated classes as tombstones
UKDFCdoHandlerhandler (cdo)CDOs, data-asset instances, allAssetsOfClass, tag matcher, actor patches
UKDFSublevelHandlerhandler (sublevel)Blocks RefinedRDLib/KBFL sublevel descriptor assets during CDOChanges
UKDFContentClassHandlerhandler baseGenerates item/resource/building/unlock/recipe/schematic/research classes; queues registration/removal
UKDFAssetHandlerhandler (asset)Decodes pack-relative 2D images into transient texture assets
UKDFDataAssetHandlerhandler (dataasset)Creates UDataAsset instances, applies instanced lists, and requests optional consumer rescans
UKDFCurveHandlerhandler (curve)Creates or edits UCurveFloat and UCurveVector assets during the Assets stage
UKDFGameTagHandlerhandler (gametag)Runtime gameplay tag registration
UKDFLocalizationHandlerhandler (localization)Runtime namespace/culture text registration
UKDFSinkPointsHandlerhandler (sinkpoints)Queues per-world AWESOME Sink table values
UKDFResourceNodeHandlerhandler (resourcenode)Registers descriptor/node-type purge rules for live worlds
UKDFActorPatchSubsystemWorldSubsystemApplies actor patches at world begin play + every spawn
UKDFContentRemoveSubsystemWorldSubsystemOrchestrates recipe sweeps and exposes per-world removal queries/counts
UKDFResourceNodeSubsystemWorldSubsystemPurges matching nodes, local mesh actors, scanner data, and radar/scanner caches
FKDFRecipeRemovalstatic/native hooksBlocks future recipe adds and removes recipes restored from server save data
AKDFChatCommandchat command/kdf report, /kdf editor, /kdf debug on|off

Resolution (FKDFPropertyPath → FKDFPropertyResolver) turns a parsed path such as mIngredients[0].Amount into a value, segment by segment, supporting name lookup (including Blueprint authored names), array/set index access, and map key lookup. Dereferencing through object-typed members is also supported, which is what makes mSomeComponent.Property and default subobjects reachable.

Conversion (FKDFValueCodec) supports bool, all numerics, enums (typed and byte-based), FString/FName/FText (with localized key, ns, and source fields), object/class references (hard and soft), structs (native text form, map form, positional sequence form), arrays, sets, and maps, with everything else falling back to a generic text-based conversion path. This fallback is what makes “every reflectable type” honest, without needing hundreds of hand-written cases.

Operations (FKDFOpEngine) validate each op against the leaf property kind before applying it: multiply on an FString produces a diagnostic, not a silent no-op. FGameplayTagContainer is special-cased so tag containers accept append/prepend/remove/clear/set with plain tag names. The full list of the 22 supported operations, their signatures, and which property kinds accept each one is covered in Operations. The property kinds themselves, and how a mIngredients[0].Amount-style path resolves, are covered in Property Types.

Before the first write to any (object, property path) pair, the original value is exported to canonical text and stored in FKDFVanillaCache. The in-game editor uses this snapshot for its Diff tab, undo workflow, and changes-only export.

applyToSpawnedActors: true on a cdo patch registers an FKDFActorPatch. UKDFActorPatchSubsystem applies the ops to all matching pre-existing actors at world begin play, and to every matching actor the moment it spawns. This guarantees YAML values win even where per-instance state, such as save-restored values or archetypes, would override plain CDO defaults. Exact-class or subclass matching is controlled by applyToSubclasses.

Actor-instance operations are direct writes. They do not compare against vanilla snapshots and do not preserve player-modified or save-restored values. The same class-scoped operation list is applied to every matching instance.

The cdo document type, its properties:/applyToSpawnedActors/applyToSubclasses fields, and the CDO/data-asset/tag-matcher targeting model are covered in full in Patching (type: cdo) and Targeting.

GameInstance handlers cannot mutate world managers because no gameplay world exists during the initial YAML pass. They retain deterministic requests instead:

  • recipe, schematic, and research entries queue SML content registration;
  • document-level remove: on those same types queues exact-class removal rules;
  • sinkpoints queues Sink table writes;
  • resourcenode queues descriptor/node-type purge rules;
  • cdo with applyToSpawnedActors queues actor-class patch rules.

UKDFWorldModule drains registrations before removals during world INITIALIZATION. SML directly removes schematic and research classes. Recipes need FKDFRecipeRemoval: process-wide hooks block future singular and batch adds, while the per-world sweep removes recipes already restored from the server save before AFGRecipeManager::BeginPlay builds derived caches. Resource Node purges use a separate four-point schedule because level actors, streamed levels, spawns, and late descriptor retargeting become visible at different times.

Both entry points, RunInitialLoad and ReloadAll, run the same pipeline:

  1. Scan. The pack scanner finds packs, manifests and files.
  2. Configuration first. Every pack’s *.config.yml definitions and its values file are loaded. Descriptions keep their identity across reloads; definitions that vanished are marked stale. The effective value of each field is decided here: a hot field follows the stored value (or the multiplayer host’s), a cold field keeps the value the running content was built with once the first load has finished.
  3. Pack conditions and dependencies. pack.yml conditions run with access to config: values. A pack that is switched off keeps its descriptions, marked not active.
  4. Parse the documents of the remaining packs.
  5. Pre-pass. Typed references and pre-pass templates are substituted in every document, and entry-level conditions: are evaluated and stripped. A document that needs session: or ags: values before a world exists is deferred. The pass also records which hot fields feed cold contexts so that the result can say restart advised.
  6. Stages run in the fixed order. Apply-time templates are expanded per operation, when the operand is about to be written.
  7. Finish. Category classes are resolved (generated classes exist only after the Items stage) and queued description events are broadcast. Events never fire in the middle of a load.

Applying changes from a UI saves pending values to the values file and, when a hot field changed, runs exactly one reload. Automatic reloads (a changed session setting, a changed host value) go through a debounce, and a burst guard suppresses runaway reload loops; user-initiated applies are never delayed.

When the world is ready, the world module tells the subsystem. If a document was deferred, or a session: / ags: / option: value that a document used has changed, KDataForge reloads once with the world’s values.

Values files are plain YAML written atomically. Only the player’s own stored values are ever written; host values and pending values stay in memory.

Dynamically created content must survive save/load with valid class paths.

Class generation relies on SML’s FClassGenerator::GenerateSimpleClass (root-set, parent layout copy, CDO auto-created). Class identity is id-based and deterministic: package /KDataForge/Gen/<PackRef>, class Gen_<PackRef>_<Id>. It is never derived from file paths or load order, so renaming a file must not orphan save data.

Classes survive save and load because they are created during GameInstance INITIALIZATION, strictly before any save loads. The save serialiser resolves class paths through StaticFindObject before attempting a linker load, so resolution hits the in-memory class. No cooked package is needed.

The constraint is that generated classes keep the parent’s layout: no new reflected properties and no new components. All variation lives in CDO defaults. This covers descriptors, recipes, schematics, and unlocks fully, but it does not cover new buildables with custom components.

A global generated-content manifest at Saved/KDataForge/GeneratedClasses.json records every generated class ever created, including its pack, id, and parent path. At startup, recorded classes missing from the current YAML are recreated as tombstones: functional but hidden and deprecated, so old saves load cleanly even after packs are removed. Because the manifest is global, class-path resolution works for any save, not just ones whose header was written by KDataForge.

Renames use redirects: in pack.yml (OldId: NewId), which feeds FCoreRedirects so old save class paths resolve to the renamed class.

Per-save state is held by AKDFStateSubsystem : AModSubsystem with SaveGame properties that store the applied-pack history: pack ref, version, and timestamp per entry.

Removal features have separate persistence rules:

  • schematic and research-tree registry removals write nothing to the save and reverse next session after the pack is removed, but do not revoke purchases or research progress;
  • recipe removal mutates the saved available-recipe list, then self-heals on a later load without the pack when purchased schematics reapply unlocks;
  • resourcenode destroys world actors, and vanilla can persist those destruction tombstones even after the pack is removed. It is a potentially irreversible world migration.

See Recipes, Schematics, Research Trees, and Resources before using those paths.

See Recipes, Schematics, and Unlocks for the document types that drive this generated-class path, and Renaming Content Safely for how redirects: is used in practice.

Every game process performs its own initial YAML load. The current source has no pack-checksum handshake, mismatch rejection, or server-to-client operation stream. Install the same packs and versions on the server and every client.

UKDFRemoteCallObject::Server_ApplySetOp is a host-gated RPC scaffold. The editor UI does not call it, and there is no general Server_ApplyOps pipeline. Treat live editor changes as local process state, not synchronized multiplayer state.

The one thing that is synchronized is pack configuration values, and nothing else:

  • The server (host or dedicated server) is the authority. AKDFConfigSyncSubsystem replicates its effective values, each flagged hot or cold, to every client.
  • A client keeps them in memory as host overrides. Its own values file is never written from them, and leaving the session restores its local values.
  • A differing hot value makes the client reload (debounced) so its content matches the host. A differing cold value only raises a host-mismatch state and event, once per key and join.
  • Changes come from the host, or from a dedicated-server administrator through Server_ApplyConfigChanges, which is permission-checked on the server and answered with Client_ConfigApplyResult. A listen-server client cannot change values.

This is value synchronization, not a pack handshake: nothing verifies that clients have the same packs. Install the same packs and versions everywhere.

  1. Stages execute in fixed EKDFStage enum order.
  2. Pack dependencies are hard topological constraints. Packs with missing, self-referential, or transitively invalid dependencies are rejected. Dependency cycles are rejected. Among packs whose dependencies are ready, higher priority runs first and ref ascending breaks ties.
  3. Within a stage, handlers are ordered by priority descending then root type ascending. Documents follow their dependency-ordered packs, then same-stage include: constraints and relative path.
  4. Map iteration never determines apply order. FKDFNode maps preserve document order.
  5. Every process must see the same pack set and inputs to produce the same result. KDataForge does not verify this across multiplayer peers yet.

For the module layout that hosts these subsystems and handlers, see Module Structure. For the YAML fields that condition evaluation, targeting, and the stage/handler pipeline actually parse, see the YAML Schema reference.