Features

Each campaign has a feature set, which is the list of campaign features that the campaign enables. Patch Manager builds the content of each campaign from the patches that its feature set allows. This page describes the Patch Manager side:

  • how to declare features from Lua

  • how to gate patches, created assets, prefab patches and run callbacks on features

  • why the gate must be on the patch, and not in an if statement around the patch

The Campaign features page describes what features are, how the game selects the feature set of a campaign, and how to gate assets.

How patching works with features

Your scripts run one time, at boot. They register all the patches that they declare. Then Patch Manager does a separate patch run for each feature set that it needs:

  • the feature set of the main menu, at boot

  • the feature set of each campaign, when that campaign loads

For each patch run, Patch Manager keeps the registered patches that the feature set allows and runs them. Then it caches the result for that feature set.

These three rules apply:

  • Each patch needs the default feature of your mod, YourModId:YourModId. You do not specify this. If your mod is not enabled in a campaign, your patches never run in that campaign.

  • A patch runs only in the feature sets that its gates allow. Add gates with :Feature, :NoFeature and PM:WithFeature.

  • Patch in place to change an asset for some campaigns only. Put the patch under a feature. The base of a patch run is the content that the asset gates let load. Patches apply to that base. A copy that keeps the id of the original is a second definition of the same asset.

Declaring a feature

PM:DefineFeature(id, options) declares a feature of your mod. options is an optional table. It can contain requires, conflicts and provides lists of feature ids, and a recommended boolean:

PM:DefineFeature("HardMode", {
    requires = { "Ksp2Redux:Parts" },
    conflicts = { "EasyMode" },
    recommended = false,
})

A bare id refers to a feature of your mod. Thus, "HardMode" is YourModId:HardMode. Call PM:DefineFeature at the top level of a script, during patch registration. A later call, for example from a Do callback, is an error.

A packaged mod can declare its features in swinfo.json instead. A Lua mod with a single file declares its features with PM:DefineFeature. Both methods put the features into the same registry. The Campaign features page explains the relations.

Gating a patch

:Feature(...) runs the patch only when all the listed features are enabled. :NoFeature(...) stops the patch when one or more of the listed features are enabled.

PM.Parts:Patch("HalveThrust")
        :Feature("HardMode")
        :Has("Module_Engine")
        :Do(function(part) ... end)

PM.Parts:Patch("RetuneForBigSystem")
        :Feature("HardMode", "KerbolarUpscale:Rescale")
        :Do(function(part) ... end)

PM.Parts:Patch("StandardTuning")
        :NoFeature("HardMode")
        :Do(function(part) ... end)

A bare id refers to a feature of your mod. Write the features of other mods with their full ids. For OR, register the patch two times with two names, and gate each patch on a different feature.

If its features stop a patch, the patch shows under Removed Patches in the summary. Its status is FEATURE, followed by the first feature that was missing, such as feature - YourModId:HardMode. For a :NoFeature, the status shows feature - not YourModId:HardMode.

Gating a block

PM:WithFeature(id, function() ... end) adds a feature to all the registrations in the function: patches, created assets, prefab patches, run callbacks and config entries. If all the patches in a file belong to one feature, put the body of the file in one PM:WithFeature call. Then you do not need to chain :Feature on each patch:

PM:WithFeature("Rescale", function()
    PM.Planets:Patch("RescaleKerbin"):Late():Named("Kerbin"):Do(rescaleBody)
    PM.Planets:Patch("RescaleMun"):Late():Named("Mun"):Do(rescaleBody)
end)

You can put scopes in other scopes. A registration in two scopes needs the features of both scopes. :Feature in a scope adds features to the features of the scope. It does not replace them.

Created assets

Each creation helper returns the queued asset. The queued asset accepts :Feature and :NoFeature, with the same rules as a patch. These are the creation helpers: PM.JSON:New, PM:New, PM.Resources:NewResource and :NewRecipe, PM.Science:NewExperiment, and PM.Planets:CreateGalaxy, :CreateAtmosphereOverride and :CreateCloudOverride.

PM.Resources:NewResource("Coolant", function(resource)
    resource.massPerUnit = 0.005
end):Feature("HardMode")

A created asset also needs the default feature of your mod, the same as a patch.

Labels shared by all feature sets

Campaign packs, game modes and difficulty settings are boot-time content. By design, they are the same in all feature sets and do not take feature gates. Patches and created assets under the campaign_pack, game_mode, difficulty and difficulty_definition labels always apply. :Feature, :NoFeature and the default feature of your mod do not stop them. Thus, a player can see the campaign pack of your mod before your mod is enabled in a campaign.

Prefab patches

PM:Prefab(...) builders also accept :Feature(...) and :NoFeature(...), with the same rules:

PM:Prefab("hard-mode-tint", "SomeWindow.prefab")
    :Feature("HardMode")
    :SetComponent("tint", "Root/Background", "UnityEngine.UI.Image", "m_Color.r", 0.5)
    :Register()

A prefab patch built in C# uses .Feature(...) and .NoFeature(...). A visual prefab patch lists its features in the Required Features and Excluded Features lists of its metadata. See Lua prefab patches and C# prefab patches.

Run callbacks

Top-level Lua variables in your script are created one time, at boot. They stay for the patch runs of all feature sets. A patch can collect data into a table in one pass and use that data in a later pass (see The patch lifecycle). Without a reset, that table keeps the data of the first feature set during the patch run of the second feature set. Run callbacks prevent this problem:

  • PM:OnBeginRun(function(set) ... end) runs before the patch run of each feature set.

  • PM:OnEndRun(function(set) ... end) runs after the patch run of each feature set.

Patch Manager gives each callback the feature set that it builds, as a table that maps each enabled id to true. Both functions return a gated action that accepts :Feature(id) and :NoFeature(id), with one id in each call. You can register both callbacks only during patch registration.

local heavyParts = {}

PM:OnBeginRun(function(set)
    heavyParts = {}
end)

PM.Parts:Patch("RecordHeavyParts"):Early():Do(function(part)
    if part.mass and part.mass > 5.0 then
        heavyParts[part.partName] = true
    end
end)

Features:Has and patch registration

The Features global gives information about the active feature set. It is for runtime code. Do not use it to select the patches to register:

-- Wrong: registers once, at boot, for every set at once.
if Features:Has("HardMode") then
    PM.Parts:Patch("HalveThrust"):Do(function(part) ... end)
end

-- Right: registered once, run only in the sets that have HardMode.
PM.Parts:Patch("HalveThrust"):Feature("HardMode"):Do(function(part) ... end)

Your script runs one time, before a feature set is active. The patches that it registers then apply to all the feature sets that the game will patch for. At that time, Features:Has returns true for all ids. Thus, the if statement above registers the patch for all campaigns, with or without hard mode. Patch Manager checks a gate on the patch again for each feature set.

Also, do not read Features:Has in a Do callback. Patch Manager caches the result of a feature set and uses it again. Gate the patch, and do not branch in the patch.

Other mods

:Needs(modId), :Conflicts(modId) and PM:Loaded count a mod only when both of these conditions are true:

  • The mod is installed.

  • The default feature of the mod is enabled in the feature set that Patch Manager builds.

During registration, no feature set is active. Thus, during registration, each installed mod counts as enabled.

See Dependencies and ordering.