Skip to content

Reference

cheese3660 edited this page Dec 11, 2023 · 8 revisions

Language Overview

Description and Motivation

The Patch Manager Language for KSP 2 is a patching format based off of a modified form of SCSS to define patches to multiple things in the game such as parts/celestial bodies/general json files. The following things motivated this formats creation

  • The old MM (Module Manager) syntax was very obscure and not easy to learn and based off of an obscure CFG format that KSP1 used to define parts/modules/what have you
  • KSP 2 does not do part "configuration" in the same way as KSP 1 in any case so MM cannot be ported into KSP 2
  • There are similarities between the old MM syntax and CSS, and the usage of "selectors" to find parts lends itself to a language based off of (S)CSS
  • Case studies showing the patching syntax to an old KSP 1 MM part patcher gave positive reactions to the syntax

Example Patch

The following patch, stored in a double_container.patch file will double the value of any parts resource container

:parts {
    * > ResourceContainers > * {
        capacityUnits: *5;
        initialUnits: *5;
    }
}

Let's go through the patch in sections to describe how everything works

:parts {

This defines a part patch, which means the inside defines a specialized set of patch rules for parts

        * > ResourceContainers > * {

This one is a bit more complex, it uses a > operator, which means that the right side is a child of the left side, and the * operator which "selects" everything. So essentially read right to left, this selects every resource container in every parts list of resource containers.

        capacityUnits: *5;
        initialUnits: *5;

These 2 do essentially the same thing, they multiply the capacityUnits/initialUnits of the resource container by 5 respectively, the prefixed * does multiplication, though it is syntactic sugar for $value*, so the following 2 lines would be identical to above in terms of function

        capacityUnits: $value*5;
        initialUnits: $value*5;

And finally the last 2 lines

    }
}

These 2 are pretty explanatory, they just end the patch

Patch Manager Language Reference

Values

There are a few primitive values in the Patch Manager Language, they correspond to JSON values mostly

Type Example Value Description
none null A completely empty value
bool true/false A boolean value, pretty self explanatory
int 123 Any integer number
real 123.45 Any non-integer number
string "Hello" Any sequence of text
list [1,2] A sequence of values
object {x:"y"} A sequence of values where each index has a name
delete @delete A value that deletes whatever it is set to

Selectors

Patch Manager uses a system of selectors to match what it is trying to transform, be it parts, bodys, etc...

Selector Example Description
:ruleset :parts Indicates a certain ruleset for matching or creation
* * Selects all elements
#name #booster* Depending on ruleset selects something by name
element ResourceContainers Depends on the current ruleset but usually selects a field of some sort
.element .methalox Depends on ruleset, same as above but usually selects stuff that has said field
selector,selector .methalox,.hydrogen Matches either selector
(selector) (part,*) Wraps a selector phrase to combine w/ other selectors
selector selector part #xyz Matches both selectors
selector>selector * > * Matches the left selector w/ things that are children of the right selection
+element +hydrogen Means that this adds the element on the righthand side to the parent
~.element ~.Gimbal Select all w/o the element
~#name ~#booster* Select all that do not have a name like such
%element %hydrogen Select all of said element if it exists, otherwise create one and select it

Some differences from CSS to note

  • Classes are able to have . in them, . is not used to split on anything
  • .x.y does not select both x and y, it insteads selects x.y
  • .x .y will select both x and y, not all elements w/ y that are descendents of an element w/ x use .x > .y for that
  • > can be used w/ more than just "elements"
  • #... can include . in the name
  • #... also has support for wildcards like * and ?

Rulesets

Patch Manager currently has the following rulesets defined

  • :resources
  • :parts
  • :json

Each of these has specific things that they do

:resources

:resource classes

Name Description
.recipe Means that this resource is a recipe rather than a regular resource
.resource Means that this resource is just a resource

Creation

The resource @new(...) constructor attribute takes 1-2 arguments, the first argument being the resource name, and the second being an optional boolean that defines wether or not this is a recipe

Resources

Resources have the following fields, and has nothing that can be "added" to them with the +element selector

Name Type Example Description
name string "ElectricCharge" The name of the resource
displayNameKey string "Resource/DisplayName/Electric Charge" The localization key for the display name
abbreviationKey string "Resource/Abbreviation/EC" The localization key for the abbreviation
isTweakable bool true TBD
isVisible bool true TBD
massPerUnit real 0.0 The mass per unit of resource
volumePerUnit real 0.0 The volume per unit of resource
specificHeatCapacityPerUnit real 0.0 The specific heat per unit of resource
flowMode int 3 The flow behaviour of this resource
transferMode int 1 The transfer behaviour of this resource
costPerUnit real 0.0 Unused: How much one unit costs
NonStageable bool true TBD
resourceIconAssetAddress string "Assets/UI/Sprites/Whitebox/WB-ICO-Battery.png" The addressables address for the resources icon
Flow Modes
Transfer Modes

Recipes

:parts

:parts classes

Modules

Resource Containers

:json

This is the most generic

Statements

Statements are what come in the braces after selectors

:parts #engine_1v_methalox_swivel {
    // Statements are in here
}

Statement Syntax

// Fields
cost: [Expression];
// Indexed field
ResourceContainers[0]: [Expression];
ResourceContainers[.methalox]: [Expression];
// setValue (Just sets this entire field to the value determined by the expression)
@set [Expression];
// Merge (Merges the values in the expression w/ this value, not really a need for this but)
@merge [Expression]
// Deletion, deletes this whatever from its parent
@delete;
// Local Variables (Always scoped to the current block)
$x: [Expression]

Expressions

There are a limited set of "Expressions" in the Patch Manager Language, as they are mostly the prefix sugar ones before "Sub Expressions"

// Literally any value defined in the values section
[SubExpression]
+[SubExpression] // Add to the original value w/ the result of expression
             
-[SubExpression] // Same as above but subtraction
                 // To define a negative value or use unary minus
                 // you need to wrap it in parentheses like (-1)
*[SubExpression] // Multiplication
/[SubExpression] // Division

Sub Expressions

[Any Value]     // Any value defined in the values table
5.0             // Example, the number 5

$VARIABLE_NAME  // Refer to a variable, there are a list of already defined variables as well
$value          // Example, the current value of the field

$current        // The current object being patched
$parent         // The parent of the current object being patched

$$FIELD_NAME    // Refers to a field on the current object
$$crewCapacity  // Example, refers to the crew capacity of the current part

([SubExpression]) // Just for precedence
($value*$value)   // Example, a multiplication operation wrapped in parentheses

-[SubExpression]  // Negate a subexpression 
                  // (if this is at the expression level just wrap it in parentheses)
-$value           // Example, the additive inverse of the current value

+[SubExpression]  // Does nothing really
+3                // Example, positive 3

![SubExpression]  // Not expression, turns a boolean from true to false and vice versa

[SubExpression]+[SubExpression] // Add 2 sub expressions
5+$y                            // Example adding 5 to a variable named y

[SubExpression]-[SubExpression] // Subtract 2 sub expressions
1-$x                            // Example subtracting x from 1

[SubExpression]*[SubExpression] // Multiplication
(5+$y)*6                        // Example

[SubExpression]/[SubExpression] // Division
1/$value                        // Example, get the inverse of the current value

[SubExpression]%[SubExpression] // Remainder
$x % 2                          // The remainder of x/2

[SubExpression]>[SubExpression] // Comparison
$x > 0                          // true if x is greater than 0 else false

[SubExpression]<[SubExpression] // Comparison
$x < 0                          // true if x is less than 0 else false

[SubExpression]==[SubExpression] // Comparison
$x = 0                          // true if x equal to 0 else false

[SubExpression]!=[SubExpression] // Comparison (glyph is "!" followed by "=")
$x != 0                          // true if x is less than 0 else false

[SubExpression]&[SubExpression]  // a and b
($x > 0) & $y                    // x and $y

[SubExpression]|[SubExpression]  // a or b
($x > 0) | $y                    // x or $y

[SubExpression][[SubExpression]] // index a value
$current["x"]                    // Example, get the field "x" out of the current "object"

[SubExpression].[name]           // Same as above
$current.x                       // Example, does the exact same as above
// More advanced stuff
[SubExpression]([SubExpression]...) // Function call
max(1,2,3,4)                        // Get the max value between 1,2,3 and 4

[SubExpression]:[name]([SubExpression]) // Member Function call
[1,2,3]:append(4)                       // Results in [1,2,3,4] 

[SubExpression] if [SubExpression] else [SubExpression] // Ternary
5 if ($x % 2) == 0 else 3            // results in 5 if x is even, otherwise 3

Special Variables

$value      - The current value of the field being patched
$current    - The current object being patched
$parent     - The parent of the current object being patched

Attributes

Patch Manager has attributes that can be attached to patch selectors beforehand

All attributes apply to all nested selectors as well, and you can't have conflicting attributes

@require 'mod-guid' // This selector will only match if there is a mod w/ mod-guid installed
@require-not 'mod-guid' // The opposite of the above attribute
@stage 'stage-name' // This selector runs during the given stage
@new(...)           // This selector generates a new piece of data based off its ruleset, the arguments are ruleset dependent

Top Level Declarations

Patch Manager also has a few top level declarations to be used

@use 'library';         // Imports a patch file called "_library.patch" into this patch
                        // (used if you want to define a set of things to be common across your patches)
                        // Any patch file that starts w/ an underscore
                        // is meant for use as a library and is not ran
$x: 5;                  // Define a top level variable, these are global for the patch file,
                        // and can only be redefined at the top level

@define-stage 'pre',5;  // Defines a stage for this patch file to use,
                        // stages are per mod so you can use them across patch files
                        // and you only have to define them in one file, you can call this one stages.patch (make sure it is not a library!)
                        // The value after is the "priority" which is any number,
                        // the priority system runs from lowest to highest, within this mod
                        // Note for using other mods stages,
                        // when using the @stage attribute,
                        // just prepend the guid to the stage name
                        // Like "com.github.x606.spacewarp:pre"
                        // Other patches using this stage outside of this mod will be run at the same time as the stage within the mod
@define-global-stage 'something', 10; // Works like above, except the priority is global, and not within the mod

Conditional Patching

In a patch block or at the top level you can do the following

@if condition
        // patch that gets ran if condition is true
@else   // Optional
        // patch that gets ran if it is false
@end

Mixins

You can template patches using mixins

@mixin scale-resource($amount: 1) {
  capacityUnits: *$amount;
  initialUnits: *$amount;
}

Which are included using @include inside a patch body

:part > ResourceContainers > * {
    @include scale-resources($amount: 5);
}

Functions

You can define functions in the Patch Manager that can be run.

@function double($value) {
    @return $value * 2;
}

Which can then be used in a patch like follows

:parts > ResourceContainers > * {
    capacityUnits: double($value); // Normal call syntax
    initialUnits: $value:double(); // "Member call" syntax
}

Functions can also have default arguments

@function $double($value: 5) {
    @return $value * 2;
}

Closures

You can also define closures, anonymous scope capturing functions, inline with values

@use 'builtin:functional';
$double: @function($value) { @return $value*2; };
$two: $double:invoke($value);

Builtin Library

Patch Manager also has a builtin library of functions to be used

(TODO)

Collection of Patch Case Studies

Part patches

Modify all resource containers

The following patch multiplies the amount of resources all parts can store by 5

:parts {
    * > resourceContainers > * {
        capacityUnits: *5;
        initialUnits: *5;
    }
}

Increase the crew capacity of parts that have crew

The following patch increases the crew capacity of all parts that can store crew by 10

:parts {
    @if $$crewCapacity  {
        crewCapacity: +10;
    }
}

Resource Patches

Define a new resource

The following patch defines a new resource called stormlight

@new("Stormlight")
:resources {
    displayNameKey: "Resource/DisplayName/Stormlight";
    abbreviationKey: "Resource/Abbreviation/SL";
    isTweakable: true;
    isVisible: true;
    massPerUnit: 0;
    volumePerUnit: 0;
    specificHeatCapacityPerUnit: 2010;
    flowMode: 4;
    transferMode: 1;
    costPerUnit: 0.8;
    NonStageable: false;
    resourceIconAssetAddress: "";
    vfxFuelType: "Pressurized";
}

Add a module to all parts

:parts {
    $name: $$partName;
    +Module_Test {
        +Data_Test {
            TestData: $name;
        }
    }
}

A full example

/* Example of a top level variable */
$minCommRange: 1000000000;

:parts {
    /* Select parts with a DataTransmitter module. */
    /* Then, take the value from DataTransmitter field 'CommunicationRange' and return it as a variable called 'commRange' */
    .Module_DataTransmitter:[$commRange: $$CommunicationRange;] {                
        /* Select only parts that have a higher comm range value than the defined minimum */
        @if $commRange > $minCommRange {
            /* Add your own MyCustomModule to selected parts */
            +Module_MyCustomModule {
                /* Add ModuleData to MyCustomModule */
                +Data_MyCustomModule {
                    /* Add your custom made class that will hold some data */
                    +MyComplexClassProperty {
                        MyString: "initial value of the property";
                        MyInt: 1;
                        MyBool: true;
                    }
                    /* Change the value of other properties */
                    MyStateStringProperty: "initial value of the property";
                    MyDefinitionStringProperty: "initial value of the property";
                }
            }
            /* Adds your module section to Parts Manager */
            PAMModuleVisualsOverride: +[
                {
                    PartComponentModuleName: "PartComponentModule_MyCustomModule",
                    ModuleDisplayName: "PartModules/MyCustomModule/Name",
                    ShowHeader: true,
                    ShowFooter: false
                }
            ];
            /* Define sorting for your module */
            PAMModuleSortOverride: +[
                {
                    PartComponentModuleName: "PartComponentModule_MyCustomModule",
                    sortIndex: 100
                }
            ];
        }
    }
}