# Class descriptions for custom modules

# Class descriptions for custom modules

While you're mostly free to define how your classes would be set up, there are some guidelines that need to be followed, restrictions to be aware of and inner workings to understand.

## Data class

### Defining your class

```csharp
[Serializable]
public class Data_MyCustomModule : ModuleData
{ .. }
```

### Set your PartBehaviourModule type reference

```csharp
public override Type ModuleType => typeof(Module_MyCustomModule);
```

### Defining module properties

* these are entries shown in the PAM

```csharp
// Toggle (true/false) property
[KSPState] // KSPState attribute tells the game to save the state of this property in the save game file
[LocalizedField("Path/To/Your/Localization/String1")] // localization string for this attribute (see 'localization' paragraph)
[PAMDisplayControl(SortIndex = 2)] // sets the sorting index for this property. Lower values are placed first
public ModuleProperty<bool> SomeToggleProperty = new(false); // value in the parentheses defines the initial value

// String property
[LocalizedField("Path/To/Your/Localization/String2")]
[PAMDisplayControl(SortIndex = 4)]
[KSPDefinition] // KSPDefinition tells this property that its value is set from the part definition json (set by Patch Manager)
public ModuleProperty<string> SomeStringProperty = new("");

// Float property - readonly
[LocalizedField("Path/To/Your/Localization/String3")]
[PAMDisplayControl(SortIndex = 7)]
[JsonIgnore] // either [KSPState] or [JsonIgnore] is needed if you want this property to be readonly
public ModuleProperty<float> SomeReadOnlyFloat = new(0, true, val => $"{val:N0} m");

// Float property - editable by players, will be built as a slider
[LocalizedField("Path/To/Your/Localization/String4")]
[PAMDisplayControl(SortIndex = 1)]
[SteppedRange(1f, 45f, 1f)] // minimum, maximum and step values
public ModuleProperty<float> SomeEditableProperty = new(1f, false, val => $"{val:N0}°"); // initial value, isReadOnly, ToStringDelegate
```

### Defining a dropdown list property

* dropdown list properties are string properties for which you define dropdown values in `OnPartBehaviourModuleInit()`

```csharp
// Dropdown property
[LocalizedField("Path/To/Your/Localization/String5")]
public ModuleProperty<string> DropdownProperty = new ModuleProperty<string>("Some value");

public override void OnPartBehaviourModuleInit()
{
    var dropdownList = new DropdownItemList();
    dropdownList.Add("some key", new DropdownItem() { key = "some key", text = "Some value" });
    dropdownList.Add("another key", new DropdownItem() { key = "another key", text = "Another value" });
    SetDropdownData(DropdownProperty, dropdownList);
}
```

### `OnPartBehaviourModuleInit()`

* runs when this module is initialized when entering Flight/OAB state

```csharp
public override void OnPartBehaviourModuleInit()
{ /* use this to initialize some values for your module, if needed */ }
```

### OAB module description

* set the description of your module for all parts it's being attached to
* description is shown in OAB and R&D while hovering over the part after pressing `SHIFT`

```csharp
public override List<OABPartData.PartInfoModuleEntry> GetPartInfoEntries(Type partBehaviourModuleType, List<OABPartData.PartInfoModuleEntry> delegateList)
{
    if (partBehaviourModuleType == ModuleType)
    {
        delegateList.Add(new OABPartData.PartInfoModuleEntry("", (_) => "Path/To/Your/Localization/String5"));

        var entry = new OABPartData.PartInfoModuleEntry("Path/To/Your/Localization/String6",
            _ =>
            {
                var subEntries = new List<OABPartData.PartInfoModuleSubEntry>();

                subEntries.Add(new OABPartData.PartInfoModuleSubEntry(
                    "Path/To/Your/Localization/String7",
                    "subentry value"
                ));

                subEntries.Add(new OABPartData.PartInfoModuleSubEntry(
                    "Path/To/Your/Localization/String8",
                    "subentry value"
                ));

                if (UseResources)
                {
                    subEntries.Add(new OABPartData.PartInfoModuleSubEntry(
                        "Path/To/Your/Localization/String/ResourceName",
                        $"{RequiredResource.Rate.ToString("N3")} /s"
                    ));
                }

                return subEntries;
            });
        delegateList.Add(entry);
    }
    return delegateList;
}
```

### Setting up resource consumptions

* Note: trigger this from `OnStart()` in the Part Component class

```csharp
public override void SetupResourceRequest(ResourceFlowRequestBroker resourceFlowRequestBroker)
{
    if (UseResources)
    {
        ResourceDefinitionID resourceIDFromName =
            GameManager.Instance.Game.ResourceDefinitionDatabase.GetResourceIDFromName(this.RequiredResource.ResourceName);
        if (resourceIDFromName == ResourceDefinitionID.InvalidID)
        {
            _LOGGER.LogError($"There are no resources with name {this.RequiredResource.ResourceName}");
            return;
        }
        RequestConfig = new ResourceFlowRequestCommandConfig();
        RequestConfig.FlowResource = resourceIDFromName;
        RequestConfig.FlowDirection = FlowDirection.FLOW_OUTBOUND;
        RequestConfig.FlowUnits = 0.0;
        RequestHandle = resourceFlowRequestBroker.AllocateOrGetRequest("MyCustomModule", default(ResourceFlowRequestHandle));
        resourceFlowRequestBroker.SetCommands(this.RequestHandle, 1.0, new ResourceFlowRequestCommandConfig[] { this.RequestConfig });
    }
}

[KSPDefinition]
[Tooltip("Whether the module consumes resources")]
public bool UseResources = true;

public bool HasResourcesToOperate = true;

[KSPDefinition]
[Tooltip("Resource required to operate this module if it consumes resources")]
public PartModuleResourceSetting RequiredResource;

public ResourceFlowRequestCommandConfig RequestConfig;
```

## Part Behaviour class

### Defining your class

```csharp
[DisallowMultipleComponent]
public class Module_OrbitalSurvey : PartBehaviourModule
{ .. }
```

### Set your PartComponentModule type reference

```csharp
public override Type PartComponentModuleType => typeof(PartComponentModule_MyCustomModule);
```

### Create Data module instance

```csharp
[SerializeField]
protected Data_MyCustomModule _dataMyCustomModule;

public override void AddDataModules()
{
    base.AddDataModules();
    _dataMyCustomModule ??= new Data_MyCustomModule();
    DataModules.TryAddUnique(_dataMyCustomModule, out _dataMyCustomModule);
}
```

### Initialize the module behaviour

```csharp
private ModuleAction _myCustomAction;

public override void OnInitialize()
{
    base.OnInitialize();

    _myCustomAction = new ModuleAction(MethodThatWillHandleTheAction);
    _dataMyCustomModule.AddAction("Path/To/Your/Localization/String/X", _myCustomAction, 1);

    if (PartBackingMode == PartBackingModes.Flight)
    {
        UpdateFlightPAMVisibility();
        _dataMyCustomModule.EnabledToggleProperty.OnChangedValue += MethodThatWillHandleThis;
    }

    if (PartBackingMode == PartBackingModes.OAB)
    { /* do stuff that's only needed in the OAB*/ }
}

private void MethodThatWillHandleTheAction()
{ /* do stuff here */}
```

### FixedUpdate loop - Flight/Map

* define stuff that needs to be executed continuously on every FixedUpdate loop. Be careful not to do expensive stuff here
* this triggers when the vessel is loaded, in Flight/Map view only

```csharp
public override void OnModuleFixedUpdate(float fixedDeltaTime)
{
    if (_dataMyCustomModule.EnabledToggleProperty.GetValue())
    { .. }

    if (someConditionMet)
    {
        UpdateFlightPAMVisibility();
    }
}
```

### Update loop

* similar to FixedUpdate, but this is a regular Update loop independent of game time
* this triggers in Flight/Map when the vessel is loaded and in OAB when the part is attached to the assembly

```csharp
public override void OnUpdate(float deltaTime)
{ .. }
```

### FixedUpdate loop - OAB

* same as `OnModuleFixedUpdate` but it triggers only in OAB

```csharp
public override void OnModuleOABFixedUpdate(float deltaTime)
{ .. }
```

### Define behaviour when the behaviour module instance will be destroyed

* cases: exiting Flight view, part has been destroyed, exiting the game

```csharp
public override void OnShutdown()
{
    _dataMyCustomModule.EnabledToggleProperty.OnChangedValue -= OnToggleChangedValue;
}
```

### Setting visibility for PAM properties

```csharp
private void UpdateFlightPAMVisibility(bool state)
{
    _dataMyCustomModule.SetVisible(_dataMyCustomModule.SomeProperty, state);
    _dataMyCustomModule.SetVisible(_dataMyCustomModule.SomeOtherProperty, true);
    _dataMyCustomModule.SetVisible(_dataMyCustomModule.YetAnotherProperty, false);
}
```

### OnEnable

* triggers when Flight view is loaded (only for the loaded vessel) and in OAB when part is added to the assembly

```csharp
protected void OnEnable()
{ .. }
```

## Part Component class

### Defining your class

```csharp
public class PartComponentModule_MyCustomModule : PartBehaviourModule
{ .. }
```

### Set your PartBehaviourModule type reference

```csharp
public override Type PartComponentModuleType => typeof(PartComponentModule_MyCustomModule);
```

### OnStart(double universalTime)

* for new vessels this will run when the Flight view is loaded
* also runs on load for every vessel currently in Flight (don't need to be loaded).
* best used for any kind of initialization of backend tasks this vessel/module needs to go through

```csharp
private Data_MyCustomModule _dataMyCustomModule;

public override void OnStart(double universalTime)
{
    if (!DataModules.TryGetByType<Data_MyCustomModule>(out _dataMyCustomModule))
    {
        _LOGGER.LogError("Unable to find a Data_MyCustomModule in the PartComponentModule for " + base.Part.PartName);
        return;
    }
    Part.RegisterModuleForUpdates(this);
    _dataMyCustomModule.SetupResourceRequest(base.resourceFlowRequestBroker);
}
```

### OnUpdate

* this starts triggering when the vessel is first placed in Flight. Doesn't trigger in the OAB before that
* once the vessel is in Flight, it will always trigger, in any view, until the part is destroyed/recovered
* use this for tasks that need to continually run, even when vessel is unloaded. Be careful not to put expensive tasks here.
* note: with the release of Redux you must *register the part module for updates*. This is to improve performance. Register it on `OnStart()` and unregister it on `OnShutdown()`, or when appropriate for your part module.

```csharp
public override void OnUpdate(double universalTime, double deltaUniversalTime)
{
    ResourceConsumptionUpdate(deltaUniversalTime);
    UpdateStatusAndState();
}
```

### Resource consumption

```csharp
private void ResourceConsumptionUpdate(double deltaTime)
{
    if (_dataMyCustomModule.UseResources)
    {
        if (GameManager.Instance.Game.SessionManager.IsDifficultyOptionEnabled("InfinitePower"))
        {
            _dataMyCustomModule.HasResourcesToOperate = true;
            if (base.resourceFlowRequestBroker.IsRequestActive(_dataMyCustomModule.RequestHandle))
            {
                base.resourceFlowRequestBroker.SetRequestInactive(_dataMyCustomModule.RequestHandle);
                return;
            }
        }
        else
        {
            if (this._hasOutstandingRequest)
            {
                this._returnedRequestResolutionState =
                    base.resourceFlowRequestBroker.GetRequestState(_dataMyCustomModule.RequestHandle);
                _dataMyCustomModule.HasResourcesToOperate = this._returnedRequestResolutionState.WasLastTickDeliveryAccepted;
            }
            this._hasOutstandingRequest = false;
            if (!_dataMyCustomModule.EnabledToggleProperty.GetValue() &&
                base.resourceFlowRequestBroker.IsRequestActive(_dataMyCustomModule.RequestHandle))
            {
                base.resourceFlowRequestBroker.SetRequestInactive(_dataMyCustomModule.RequestHandle);
                _dataMyCustomModule.HasResourcesToOperate = false;
            }
            else if (_dataMyCustomModule.EnabledToggle.GetValue() &&
                base.resourceFlowRequestBroker.IsRequestInactive(_dataMyCustomModule.RequestHandle))
            {
                base.resourceFlowRequestBroker.SetRequestActive(_dataMyCustomModule.RequestHandle);
            }
            if (_dataMyCustomModule.EnabledToggleProperty.GetValue())
            {
                _dataMyCustomModule.RequestConfig.FlowUnits = (double)_dataMyCustomModule.RequiredResource.Rate;
                base.resourceFlowRequestBroker.SetCommands(_dataMyCustomModule.RequestHandle, 1.0,
                    new ResourceFlowRequestCommandConfig[] { _dataMyCustomModule.RequestConfig });
                this._hasOutstandingRequest = true;
                return;
            }
        }
    }
    else
    {
        _dataMyCustomModule.HasResourcesToOperate = true;
    }
}
```

### OnShutdown

* define behaviour when the Part Component instance will be destroyed
* cases: part has been destroyed, exiting the game

```csharp
public override void OnShutdown()
{ .. }
```