Skip to content
k-mods.com

API Design

Public C++ handler contract in KDataForgeApi and the KDataForge registration surface used by third-party mods.

Third-party handler types derive against KDataForgeApi. Registering those handlers calls UKDFSubsystem, which lives in the main KDataForge module, so a registering mod links both modules. Other implementation details may change between versions.

IKDFDataEditorHandler, UKDFHandlerBase, FKDFNode, EKDFStage, EKDFOp, the context structs, and FKDFPatchRecord form the public handler contract. Additive changes are preferred. The main module exposes the registration and integration methods described below.

The YAML schema is versioned through the documentation rather than C++ compatibility rules. Breaking schema changes belong in the changelog.

Implement IKDFDataEditorHandler, or normally derive from UKDFHandlerBase, for one YAML root type. The interface is C++ only because UKDFDataEditorHandler declares CannotImplementInterfaceInBlueprint.

class IKDFDataEditorHandler
{
virtual FName GetRootType() const = 0;
virtual EKDFStage GetStage() const = 0;
virtual int32 GetPriority() const;
virtual bool ValidateDocument(const FKDFNode&, FKDFValidationContext&) = 0;
virtual bool ApplyDocument(const FKDFNode&, FKDFApplyContext&) = 0;
virtual bool ExportObject(UObject*, FKDFNode&, FKDFExportContext&); // optional
virtual UClass* GetEditorPanelClass() const; // optional
};

There is no RevertDocument virtual in the current interface. The framework parser handles raw YAML loading. ApplyDocument performs writes, ValidateDocument performs structural checks, ExportObject supports editor export, and GetEditorPanelClass can provide a custom panel.

Handlers receive an order-preserving map, sequence, and scalar tree rather than YAML text. Key helpers include Find, FindShared, GetString, GetBool, GetInt, GetFloat, their TryGet* variants, and the MakeMap, MakeSequence, and MakeScalar builders used by exporters.

Contexts carry mGameInstance, mSourceFile, and a diagnostic sink with AddInfo, AddWarning, and AddError. Diagnostics are non-fatal. FKDFApplyContext adds bDryRun, mPatchRecord, and mAppliedOpCount.

KDataForgeApi also exports the extension point for {prefix:body} templates: IKDFTemplateProvider (GetPrefix, GetPhase, Resolve), the request and result structs, and the thread-safe FKDFTemplateProviderRegistry (Register, RegisterWithSpelling, Unregister, Find, GetPrefixes, GetPrefixSpellings). The typed value that providers return and that the Blueprint Config API exchanges is FKDFConfigValue (with EKDFConfigValueType and FKDFConfigKeyedValue); both live in KDFConfigTypes.h.

There is no bLiveReload flag in the handler API. KDataForge scans YAML during GameInstance initialization and again when an authorized host runs /kdf reload. Reload restores captured top-level property baselines before rebuilding and applying the handler pipeline. Frozen SML registrations and irreversible world changes still require a restart.

Register during your game instance module’s CONSTRUCTION phase. Explicit subsystem initialization is required because SML can dispatch module lifecycle phases before custom GameInstance subsystems have been created.

#include "Module/GameInstanceModuleManager.h"
#include "Subsystems/KDFSubsystem.h"
UGameInstanceModuleManager* Modules =
GetGameInstance()->GetSubsystem<UGameInstanceModuleManager>();
if (Modules != nullptr)
{
Modules->EnsureSubsystemInitialized(UKDFSubsystem::StaticClass());
if (UKDFSubsystem* KDF = UKDFSubsystem::Get(GetGameInstance()))
{
KDF->RegisterHandler(NewObject<UMyHandler>(KDF));
}
}
  • Add KDataForgeApi for handler types and KDataForge for registration to your module dependencies.
  • The subsystem retains handler objects against garbage collection.
  • Registration after the initial load logs a warning. The documents apply on the next session.
  • Multiple handlers may claim the same root type. They all run in priority order.

If you only want your mod’s content to be editable, you do not need a custom handler. Reflected properties are already reachable through built-in cdo documents using target, allAssetsOfClass, and matchTag. Recommend a mod-presence condition:

conditions:
hasMod: YourModRef

Handler registration already links the main module. Handlers that generate content or integrate with the editor can also use these stable, additive entry points:

APIPurpose
GetDynamicContent().GetOrCreateClass / GetOrCreateAssetCreate save-stable generated classes or UDataAsset instances
QueueContentRegistration / QueueSinkTableRegistrationQueue per-world SML content registration before the freeze
RegisterActorPatch / GetActorPatches / ClearActorPatchesManage spawn-time FKDFActorPatch rules consumed by UKDFActorPatchSubsystem
RegisterNodePurge / GetNodePurges / ClearNodePurgesManage FKDFNodePurge rules consumed by UKDFResourceNodeSubsystem
RegisterContentRemoval / GetContentRemovals / ClearContentRemovalsManage exact-class FKDFContentRemoval rules for recipe, schematic, and research content
MarkDataAssetsChangedRequest optional KAPI or RefinedRDApi consumer rescans after initial load
ApplyEditorOp / RestorePropertyTextApply editor writes and restore canonical property text for undo
ExportObjectToYamlExport an object to full or changes-only cdo YAML
OnToggleEditorRequested()Multicast hook used by the in-game editor UI
FKDFPatchUtil::ApplyOpsToObjectApply a parsed properties: sequence with pipeline semantics
UKDFContentRemoveSubsystemApply recipe cleanup for one world and query exact removal rules/counts

The three Register* rule families above are retained on UKDFSubsystem for the GameInstance. Registering a rule with the same target class and source file replaces the previous rule. Clearing a rule array only stops later consumers; it does not undo actor writes, restore destroyed resource nodes, or put removed content back into an already-initialized manager.

  • RegisterActorPatch is normally driven by applyToSpawnedActors: true. At world begin play, UKDFActorPatchSubsystem applies matching rules to existing actors; its spawn delegate handles later actors. A rule registered after that initial sweep affects future spawns. Call ApplyPatchesToActor explicitly when a late integration also needs an existing actor updated.
  • RegisterNodePurge is normally driven by type: resourcenode. World initialization, actor-spawn, streamed-level, and deferred sweeps consume the rules. There is no public on-demand whole-world sweep, and destroyed level nodes can already have persistent vanilla save tombstones.
  • RegisterContentRemoval is normally driven by document-level remove:. Schematic and research filtering happens while RegisterQueuedContentForWorld drains the SML registry before its freeze. Recipe rules are different: native add hooks prevent later grants, and UKDFContentRemoveSubsystem::ApplyWorldRemovals can re-run its idempotent save-restored-recipe sweep when the rule set grows. Calling it late does not retroactively remove a schematic or research tree from a registry that already initialized.

UKDFContentRemoveSubsystem::Get(WorldContext) exists only for Game and PIE worlds. Its IsRecipeRemoved and IsContentRemoved queries use exact class identity, not ancestry. GetAppliedRemovalCount reports resolvable rules seen by the last application pass, not the number of manager entries changed. WereRemovalsApplied becomes true after the first non-empty application pass and clearing the GameInstance rule list does not reset that per-world history flag.

FKDFRecipeRemoval exposes lower-level HasAnyRemovalRules, IsRecipeRemoved, and the server-only SweepSaveRestoredRecipes helper. Normal integrations should use UKDFContentRemoveSubsystem so world timing and reporting stay centralized. Native recipe-add prevention is compiled for packaged runtime builds, not Unreal Editor PIE; PIE still exercises the initialization sweep.

Treat unlisted implementation details in the main module as internal.

KDataForgeApi follows the mod’s CI version. Handler ABI additions should be backwards compatible. Breaking schema changes must update the YAML Schema and the changelog.