Mother DocsMother Docs
Buy me a Coffee
Steam Workshop
Discord
  • Mother OS (Ingame Script)
Buy me a Coffee
Steam Workshop
Discord
  • Mother OS (Ingame Script)
  • Mother OS (Ingame Script)
    • Getting Started

      • Upgrade Guide
      • Installation
      • Command Line Interface (CLI)
      • Configuration
      • Modules
    • Core Modules

      • Activity Monitor
      • Almanac
      • Block Catalogue
      • Local Storage
      • Intergrid Message Service
    • Extension Modules

      • Air Vent Module
      • Battery Module
      • Terminal Block Module
      • Cockpit Module
      • Connector Module
      • Display Module
      • Docking Module
      • Door Module
      • Flight Control Module
      • Flight Planning Module
      • Gas Tank Module
      • Gyroscope Module
      • Hinge Module
      • Landing Gear Module
      • Light Module
      • Map Module
      • Piston Module
      • Programmable Block Module
      • Rotor Module
      • Screen Module
      • Sensor Module
      • Sorter Module
      • Sound Block Module
      • Thruster Module
      • Timer Block Module
    • Command Cheatsheet
    • Compatibility
    • Examples
  • Mother Core (Script Framework)
    • Getting Started

      • Upgrade Guide
      • Installation
      • Architecture Overview
      • Managing Script Size & Complexity
    • Building A Module
    • Console (CLI)
    • Core Modules
      • Activity Monitor
      • Almanac
      • Block Catalogue
      • Clock
      • Command Bus
      • Configuration
      • Event Bus
      • Intergrid Message Service
      • Local Storage
      • Terminal
    • Utilities

      • Color Helper
      • Security
      • Serializer
  • Powered By Mother

Building a Module

You will be creating an Extension Module. These are the main components of your scripts. This section will cover the basics of creating an extension module, registering custom terminal commands, and handling events.

  • Creating a Module
    • Registering a Module
    • Booting a Module
    • Running a Module
  • Terminal Commands
    • Creating a Command
    • Customizing a Command
    • Registering a Command
  • Events
    • Creating an Event
    • Emitting an Event
    • Subscribing to an Event
  • Accessing Blocks
    • Getting Blocks From the Grid
    • Monitoring Blocks For Changes

Creating a Module

Note

Mother OS is a collection of extension modules built on top of Mother Core.

Use the make:module command from within your project directory to create a new module in the /Modules directory. Let's create the MissileGuidanceModule module.

Console/Terminal
mother make:module MissileGuidanceModule
ExampleProject/
├── Program.cs
├── thumb.png
├── Modules/
    ├── MissileGuidanceModule/
        ├── MissileGuidanceModule.cs

Registering a Module

Mother makes it easy to register Extension Modules via the RegisterModule() or RegisterModules() methods. This ensures our module is accessible when Mother boots. We register the module in the Program constructor of your script.

And then we register it in the Program constructor:

Program.cs
partial class Program : MyGridProgram
{
    private Mother mother;

    public Program()
    {
        // Create Mother instance
        mother = new Mother(this);

        // Register module with Mother
        mother.RegisterModules(new List<IExtensionModule> { 
            new MissileGuidanceModule(mother),
            ...
        });
    }
}

Important

Extension Modules must conform the the IExtensionModule interface. It is recommend that you use the BaseExtensionModule class as a base class to leverage the library of useful helper methods.

Booting a Module

The Boot() method of every module is called during the boot process. Core Modules are booted before Extension Modules in the order they are registered. It is important to consider boot order to reduce conflicts among module dependencies and leverage Events to keep modules decoupled.

MissileGuidanceModule.cs
class MissileGuidanceModule : BaseExtensionModule
{
    public void Boot()
    {
        // Register commands
        RegisterCommand(new LaunchCommand())
        RegisterCommand(new DetonateCommand())

        // Subscribe to events
        Subscribe<WaypointReachedEvent>();
        Subscribe<ReadyForLaunchEvent>();

        // Boot activities
        ConfigureWarheads();
    }
}

Running a Module

You may also run processes every program cycle using the Run() method. This is only recommended for specific use cases where you need the module to run 6 times per second. Otherwise it is advised to use the Clock's Schedule() method to control the frequency, or use an Event when you want your module to respond to an activity.

MissileGuidanceModule.cs
class MissileGuidanceModule : BaseExtensionModule
{
    // Runs every program cycle automatically
    public void Run()
    {
        // Update the current position of the missile
        DetermineCurrentPosition();

        // Update the thrusters based on the current position
        UpdateThrusters();

        // Check if we are within the terminal range
        if(IsWithinTerminalRange())
        {   
            ArmWarheads();
        }
    }
}

Terminal Commands

Custom terminal commands are easily registered from within modules. Commands implement the IModuleCommand interface and are registered in the Boot() method of the module.

Creating a Command

Use the make:command command from within your project directory to create a new command. You may provide an optional module name if you wish to associate the command with a specific module.

Let's create the LaunchCommand command.

Console/Terminal
mother make:command LaunchCommand --module MissileGuidanceModule
ExampleProject/
├── Program.cs
├── thumb.png
├── Modules/
    ├── MissileGuidanceModule/
        ├── MissileGuidanceModule.cs
        ├── Commands/
            ├── LaunchCommand.cs

Info

Mother automatically registers the command in the module's Boot() method when used in the make:command command.

Running the make:command command without a module option will create a command in the /Commands folder of your project.

Console/Terminal
mother make:command HaltAndCatchFireCommand
ExampleProject/
├── Program.cs
├── thumb.png
├── Modules/
├── Commands/
    ├── HaltAndCatchFireCommand.cs

Customizing a Command

First we define the command's Name. This is what the player will use to call the command from the terminal.

LaunchCommand.cs
public class LaunchCommand : BaseModuleCommand
{
    // The name of the command
    public string Name => "launch";
    // or we can use a namespace to organize our 
    // commands by function or block   
    public string Name => "missile/launch";
}

We can instantiate the command with the parent module for easy reference.

LaunchCommand.cs
public class LaunchCommand : BaseModuleCommand
{
    MissileGuidanceModule Module;

    public LaunchCommand(MissileGuidanceModule module)
    {
        Module = module;
    }
}

Now we implement the Execute() method, which will be called when the command is executed via a player command or other trigger. The method takes a TerminalCommand object as the only parameter and returns a string which will be printed in the terminal.

Let's imagine we run the following command in our Programmable Block terminal to launch a missile, which includes a target position, and option for max speed:

Programmable Block Terminal
launch 24422.23,32334.56,10045.33 25 --maxSpeed=50

The TerminalCommand object will contain the command name, arguments, and options. We can access these properties to get the information we need to execute the command.

LaunchCommand.cs
public class LaunchCommand : BaseModuleCommand
{
    public string Execute(TerminalCommand command)
    {
        // first argument
        string targetCoordinate = command.Arguments[0];

        // second argument as a double
        double detonationDistance;
        double.TryParse(command.Arguments[1], out detonationDistance);

        // get an option ie. --maxSpeed=50
        string maxSpeed = command.GetOption("maxSpeed");

        // call InitiateLaunch() method on parent module. The parent will 
        // handle the core launch logic and may fire an event 
        // that other modules can listen for.
        bool success = Module.InitiateLaunch(
            targetCoordinate, 
            detonationDistance, 
            maxSpeed
        );

        // return a message to the terminal for the player
        if(success)
            return "Missile Launched!";
        else
            return "Missile Launch Failed!";
    }
}

Registering a Command

To register a command, we use the RegisterCommand() method. We define it in the Boot() method of the parent module. This method can accept an instance of the module to allow access to its specialized methods from within the command.

MissileGuidanceModule.cs
public class MissileGuidanceModule : BaseExtensionModule
{
    public void Boot()
    {
        // Register command with access to the current module
        RegisterCommand(new LaunchCommand(this));
    }
}

Events

Events allow modules to communicate with each other without being tightly coupled. Modules can emit events, then other modules may respond and take action. This keeps our scripts modular.

Creating an Event

Use the make:event command from within your project directory to create a new event. You may provide an optional module name if you wish to associate the event with a specific module.

Let's create the MissileLaunchedEvent event.

Console/Terminal
mother make:event MissileLaunchedEvent --module MissileGuidanceModule
ExampleProject/
├── Program.cs
├── thumb.png
├── Modules/
    ├── MissileGuidanceModule/
        ├── MissileGuidanceModule.cs
        ├── Events/
            ├── MissileLaunchedEvent.cs

Running the make:event command without a module option will create an event in the /Events folder of your project.

Console/Terminal
mother make:event EnemySpottedEvent
ExampleProject/
├── Program.cs
├── thumb.png
├── Modules/
├── Events/
    ├── EnemySpottedEvent.cs

Emitting an Event

Modules can emit events using the Emit() method. This method takes an IEvent instance as a parameter, and an optional object of event data.

When we launch our missile, we should emit the MissileLaunchedEvent event.

MissileGuidanceModule.cs
    public void Launch()
    {
        // Emit the event using the event Type, and event data  
        object eventData;
        Emit<MissileLaunchedEvent>(eventData);

        // Or, we can emit without event data (nice a simple!)
        Emit<MissileLaunchedEvent>();
    }

Note

The Emit() and Subscribe() methods are accessors for the Event Bus via the BaseExtensionModule class.

Subscribing to an Event

Once subscribed to an event, a module take action via the HandleEvent() method each time that event is emitted. Let's imagine we have a WarheadModule that needs to arm the warhead when a missile is launched.

ExampleProject/
├── Program.cs
├── thumb.png
├── Modules/
    ├── WarheadModule/
        ├── WarheadModule.cs
    ├── MissileGuidanceModule/
        ├── MissileGuidanceModule.cs
        ├── Events/
            ├── MissileLaunchedEvent.cs

We subscribe to this event in our module's Boot() method. Then we use the HandleEvent() method to handle the event each time it is emitted.

WarheadModule.cs
public class WarheadModule : BaseExtensionModule
{
    // Subscribe to the event during boot
    public override void Boot()
    {
        Subscribe<MissileLaunchedEvent>();
    }
 
    // Handle the event when it is emitted by this, or another module
    public override void HandleEvent(IEvent e, object eventData)
    {
        if(e is MissileLaunchedEvent) {
           Mother.Print("Missile has launched, arming warhead!");

           ArmWarhead();
        }
    }

    void ArmWarhead() {
        // Red light, green light
    }
}

Accessing Blocks

Getting Blocks From the Grid

The Block Catalogue makes a ledger of all blocks on the current grid during boot. This allows us to access these blocks more efficiency when updated via commands or events. It also simplifies access to block configuration and state monitoring.

Important

Mother treats all blocks connected via hinges, rotors, and pistons as a single construct. It is fully compatible with subgrids and will not interfere with blocks on other grids via connector connections (ie. a docked ship). Your automations will not interfere with other ships when connected via connectors.

Any IMyTerminalBlock on your construct can be accessed via the GetBlocks() method. You can get a block by its type, and use an optional action for filtering the retrieved blocks.

Tips

See Malware's API Index for more information on block types.

MissileGuidanceModule.cs
public class MissileGuidanceModule : BaseExtensionModule
{
    public void Boot()
    {
        // Get the Block Catalogue core module
        BlockCatalogue BlockCatalogue = Mother.GetModule<BlockCatalogue>()

        // Get all thrusters on the grid
        List<IMyThrust> thrusters = BlockCatalogue
            .GetBlocks<IMyThrust>();

        // or, only get blocks where name contains a key
        List<IMyThrust> thrusters = BlockCatalogue
            .GetBlocks<IMyThrust>(block => block.CustomName.Contains("door"));
    }
}

The more practical situation is when we want to get a specific block on the grid by it's name, or multiple blocks within a group. This can be done by via the GetBlocksByName() method. It accepts a block type, and a string for the block, group, or tag name.

MissileGuidanceModule.cs
public class MissileGuidanceModule : BaseExtensionModule
{
    public void Boot()
    {
        // Get the Block Catalogue core module
        BlockCatalogue BlockCatalogue = Mother.GetModule<BlockCatalogue>()

        // Get the thrusters in the group "Booster Thrusters"
        List<IMyThrust> thrusters = BlockCatalogue
            .GetBlocksByName<IMyThrust>("Booster Thrusters");

        // or, get a specific block by name
        IMyThrust retroThruster = BlockCatalogue
            .GetBlocksByName<IMyThrust>("RetroThruster")
            .firstOrDefault();

        // or even via a tag
        IMyThrust taggedThruster = BlockCatalogue
            .GetBlocksByName<IMyThrust>("#tagged-thruster")
            .firstOrDefault();
    }
}

Monitoring Blocks For Changes

Blocks in Motion

Blocks that move can leverage the Activity Monitor to monitor their changing state ie. angle, distance. In the case of hinges, we can set a hinge in motion, and then stop and lock it in place when it reaches the specified angle.

For blocks that change state infrequently between discrete values (ie. VentStatus.Pressurizing, MyShipConnectorStatus.Connected), you can monitoring for state changes instead.

Before our missile launches, we should ensure the exhaust flaps are open.

MissileGuidanceModule.cs
const float HINGE_OPEN_ANGLE = 45f; // degrees
const string STATE = "READY_FOR_LAUNCH";

public void InitiateLaunch(
    string targetCoordinate, 
    double detonationDistance, 
    float maxSpeed
)
{
    // Get the hinge blocks
    var hinges = Mother.GetModule<BlockCatalogue>()
        .GetBlocksByName<IMyMotorStator>("ExhaustHinges")

    // Set target, detonation distance, and max speed
    // ...

    // Open exhaust flaps
    hinges.ForEach(hinge => {
        Mother.GetModule<ActivityMonitor>()
            .RegisterBlock(
                // we pass in the hinge block
                hinge,
                // We specify the condition to run 
                // against the hinge each cycle
                // ie. has it reached the desired angle?
                block => HingeAtDesiredAngle(
                    block as IMyMotorStator, 
                    HINGE_OPEN_ANGLE
                ),
                // we specify the action to take when the above condition is 
                // true, which can access the block if desired
                // ie. the hinge has reached the desired angle
                block => LaunchMissile()
            );
    });
}

void LaunchMissile()
{
    // We ensure that launch can only be called once since multiple hinges 
    // will call this method as they reach their terminal position. The 
    // ability to monitor the aggregate completion of a group of 
    // blocks will come in a future update.
    if(LAUNCH_STATE == "LAUNCH")
        return;

    LAUNCH_STATE = "LAUNCH";
    
    // logic to start thrusters, engage autopilot, etc...
}

Tips

The hinge/rotate command is an example of where the activity monitor is used. Mother OS uses the Activity Monitor to track the motion of hinges, rotors and pistons.

Block State Changes

All blocks on the grid can also be monitored for state changes. The state value of blocks varies by the block type, so we will define the property to watch, and define an action to handle the state change when it occurs. We do this in the Boot() method of our module:

Warning

Use this capability with caution as this adds more computation per game cycle. Blocks being monitored like this will always be checked for state changes, even if they are not in motion. This is useful for blocks that change state infrequently, such as connectors, doors, and landing gear.

You should only monitor individual blocks - NOT groups, or tags.

MissileGuidanceModule.cs
public void Boot()
{
    // Register connectors and monitor the Status property which 
    // tells us if the connector is connected or not.
    RegisterBlockTypeForStateMonitoring<IMyShipConnector>(
        // We specify the block's property to monitor (Status)
        connector => connector.Status, 
        // We specify the action to take when the block's state changes
        (block, newState) => HandleConnectorStateChange(
            block as IMyShipConnector, 
            newState
        );
    );
}

We will create the HandleConnectorStateChange() method to accept the new state of the connector, and emit an event upon change. This method will only be called if the state of the connector has changed.

MissileGuidanceModule.cs
public void HandleConnectorStateChange(IMyShipConnector connector, object newState)
{
    // We cast the new state to the specific type we are monitoring
    var status = newState as MyShipConnectorStatus?;

    if(status.hasValue)
    {
        switch(newState)
        {
            // The connector is now connected
            case ConnectorStatus.Connected:
                Emit<ConnectorLockedEvent>(connector);
                break;
                
            // The connector is now disconnected
            case ConnectorStatus.Unconnected:
                Emit<ConnectorUnlockedEvent>(connector)
                break;
        }
    }
}

Note

In Mother OS, a connector's hooks are activated by the connector's state change using the method above.

Last Updated: 9/28/25, 8:12 PM
Contributors: Luke Morrison, lukejamesmorrison
Prev
Managing Script Size & Complexity
Next
Console