Things you can do in your plugin

From Torch Wiki
Jump to: navigation, search

This tutorial explains some of the mechanics you can use in your plugins and explains how exactly they work.

Creating Commands

One of the most basic uses for a plugin is the CommandSystem. It allows to trigger certain functions of your Plugin, can provide feedback to the User.

How to register a command

Torches PluginManager is Responsible for loading, and updating Plugins. After you Plugin was loaded, your Plugin is checked again for any classes that contains commands to be Registered by the CommandManager.

The same Rules as for you Plugin itself apply here. Your command class has to be public and it needs to extend the CommandModule class.

1 using Torch.Commands;
2 
3 namespace TestPlugin {
4 
5     public class TestCommands : CommandModule {
6 
7     }
8 }

However without commands its not really necessary to do so.

Add a command

 1 using Torch.Commands;
 2 using Torch.Commands.Permissions;
 3 using VRage.Game.ModAPI;
 4 
 5 namespace TestPlugin {
 6 
 7     public class TestCommands : CommandModule {
 8 
 9         [Command("test", "This is a Test Command.")]
10         [Permission(MyPromoteLevel.Moderator)]
11         public void Test() {
12 
13         }
14     }
15 }

Basically every public void method you add to your commands class can become a command. All you need to do is to add the Command annotation to it.

It takes two parameters. Name of the command, and a command description.

  • Name
    • The name is the name a player and yourself uses to call this method. In the case above the command can be called using !test.
  • Description
    • The Description is shown in game when a player uses the !longhelp command. You can use it for describing what your command does and how it works. But keep it simple. Take the commands of other plugins or torch itself as an example.

Permission levels

Each and every Command can have a permission level attached to it. This Permission Level limits who may have access to this command.

There are a few values to choose from:

  • None
    • Everyone can use it
  • Scripter
    • Only players with Scripter (*) role and above can use it.
  • Moderator
    • Only players with Moderator (**) role and above can use it.
  • Space Master
    • Only players with Space master (***) role and above can use it.
  • Admin
    • Only players with Admin (****) role and above can use it.
  • Owner
    • Only players added to the server config file (*****) and the console can use it.

Always choose wisely who gets access to which command. The perfect permission level depends on what the command does. For example: Teleport should probably not be available for everyone. But a Message of the Day should.

If you want your command to behave differently depending on who calls it you may use "None" but have to check for permissions yourselves.

Command Groups

If you have multiple commands that belong together you can group them together. One way of doing so is to annotate the whole command class:

1     [Category("torch")]
2     public class TestCommands : CommandModule {

This causes all your commands to get a prefix. In this case you can only call our test command by typing in !torch test

This application is useful if you for example have a save, load, reload etc. function in your plugin. Also it helps your players with identifying the commands belonging to the same thing more easy.

Arguments

Most of the time a command does not really work by itself. You need to add parameters to it. For example, which player, which grid, which whatever should the command take affect on.

there are two ways of doing so. Either you add the commands as parameters to your method or you use the CommandContext for that. The advantage of using string parameters is that torch handles argument validation for you. So if the number of arguments is incorrect or the types are wrong the user will get a feedback for that.

1         [Command("testWithCommands", "This is a Test Command.")]
2         [Permission(MyPromoteLevel.None)]
3         public void TestWithArgs(string foo, string bar = null) {
4             Log.Info("This is a Test "+ foo +", " + bar);
5         }

In this case we have a parameter foo that needs to be filled out and an optional parameter bar.

Commands class life cycle

Your commands class has a very short life cycle. Unlike your Plugin that is instantiated once at startup the CommandManager just holds a register of which class to instantiate if a certain command is added.

So every time a user enters a command a new instance of your commands class is created. After after the command has finished executing the class will be thrown out again.

Therefore it is impossible to save any state to your class. Everything you need to save for later use you have to save to a different object that remains.

One easy way to do so is getting your Plugins reference.

1     public class TestCommands : CommandModule {
2 
3         public TestPlugin Plugin => (TestPlugin) Context.Plugin;

The Context holds information on who triggered the command and which plugin did so. So if you don't have any static reference to your plugin this one will allow you to get one.

Working with the CommandContext

The CommandContext is a Property you have access to through the CommandModule.

1         [Command("test", "This is a Test Command.")]
2         [Permission(MyPromoteLevel.Moderator)]
3         public void Test() {
4             Log.Info("This is a Test from "+ Context.Player);
5         }

It has the following Properties:

  • Plugin
    • Has the reference of the plugin that registered the command
  • Torch
    • Has the reference of torch itself
  • Player (Type IMyPlayer)
    • hold the reference of the Player that executed the command
    • Warning: If the console (Torch UI) executed the command the Player is NULL
    • If the Player is not Null you can Cast it to MyPlayer if you need a few more functionality. After all you are not bound to any Interfaces if you don't want to
  • Args
    • Array of arguments passed to the command.
  • RawArgs
    • Arguments passed to the command unparsed.

User Responses

The Command Context also has a Response Method. You can call it how often you Like. The CommandManager translates it into private messages to the Player or console running the command so no one else can see them.

1             Context.Respond("This is a Test from "+ Context.Player);

Full Example

 1 using Torch.Commands;
 2 using Torch.Commands.Permissions;
 3 using VRage.Game.ModAPI;
 4 
 5 namespace TestPlugin {
 6 
 7     [Category("torch")]
 8     public class TestCommands : CommandModule {
 9 
10         public TestPlugin Plugin => (TestPlugin) Context.Plugin;
11 
12         [Command("test", "This is a Test Command.")]
13         [Permission(MyPromoteLevel.Moderator)]
14         public void Test() {
15             Context.Respond("This is a Test from "+ Context.Player);
16         }
17 
18         [Command("testWithCommands", "This is a Test Command.")]
19         [Permission(MyPromoteLevel.None)]
20         public void TestWithArgs(string foo, string bar = null) {
21             Context.Respond("This is a Test "+ foo +", " + bar);
22         }
23     }
24 }

Now after Compiling and Updating the Plugin you notice the following entries on your console:

19:35:07.7620 [INFO]   CommandManager: Registering command 'torch.test'
19:35:07.7620 [INFO]   CommandManager: Registering command 'torch.testWithCommands'

Which means your new Commands are now available and ready to use.

19:36:49 Server: !torch test
19:36:49 Server: This is a Test from
19:37:12 Server: !torch testWithCommands foo bar
19:37:12 Server: This is a Test foo, bar

Loading and Saving Config

Most of the time you create a plugin, you want to offer the Server Administrator that uses your plugin to alter some behavior.

For example: instead of deleting Ships that are older than 5 days, maybe they want 10. Or you want to disable your plugin temporarily, allow them to enter user credentials and what not. For that you need two things:

  1. An user interface to change these settings
  2. A way to save and load these settings

An UI is optional, so we take a look at saving and loading your Changes first.

Creating the ViewModel

Configuration are Saved in a ViewModel. This is a class that extends Torches ViewModel and holds the data you want to save and load.

As you may have guessed: this class also needs to be public in order for the serialization to find it and deal with it.

1 using Torch;
2 
3 namespace TestPlugin {
4     public class TestConfig : ViewModel {
5 
6     }
7 }

Torch is able to read this classes properties automatically and create an XML file with it, and offers PropertyChange Events you could listen to if needed. However Torch handles it all for you so you most likely do not.

In this example We fill it with a few fields of different types:

 1 using Torch;
 2 
 3 namespace TestPlugin {
 4     public class TestConfig : ViewModel {
 5 
 6         private string _Username = "root";
 7         private string _Password = "";
 8         private int _AuthToken = 0;
 9         private bool _PreferBulkChanges = true;
10 
11         public string Username { get => _Username; set => SetValue(ref _Username, value); }
12         public string Password { get => _Password; set => SetValue(ref _Password, value); }
13         public int AuthToken { get => _AuthToken; set => SetValue(ref _AuthToken, value); }
14         public bool PreferBulkChanges { get => _PreferBulkChanges; set => SetValue(ref _PreferBulkChanges, value); }
15     }
16 }

The fields are for storing the data and assinging default Values. Especially if you add new configuration later on it would be a good idea to have some kind of default.

The Proerties are used for the UI, to bind to, the plugin itself to read from, and the serialization to save stuff. You can of course make it more compact but this demonstrates the idea of how it works.

Including the Config into your Plugin

We now alter our plugins class to include the new config.

First we add two new attributes to our class:

1 using Torch;
2 
3 namespace TestPlugin {
4     public class TestConfig : ViewModel {
5 
6         private Persistent<TestConfig> _config;
7         public TestConfig Config => _config?.Data;
8     }
9 }

Persistent is the object Torch uses to Serialize your Config to XML. Our config itself is in the Persistents Data.

Now we need to initialize them:

 1         private void SetupConfig() {
 2 
 3             var configFile = Path.Combine(StoragePath, "TestConfig.cfg");
 4 
 5             try {
 6 
 7                 _config = Persistent<TestConfig>.Load(configFile);
 8 
 9             } catch (Exception e) {
10                 Log.Warn(e);
11             }
12 
13             if (_config?.Data == null) {
14 
15                 Log.Info("Create Default Config, because none was found!");
16 
17                 _config = new Persistent<TestConfig>(configFile, new TestConfig());
18                 _config.Save();
19             }
20         }

The StoragePath comes from the TorchPluginBase itself. And by default points to the instances folder within your torch install. And TestConfig.cfg is the name of our config file within the instances folder.

However we may have problems loading it, common example would be if it does not exist. In that case we create a new instance of Persistent, with our TestConfig in it and save it with its default values.

When ever something bad happens it is useful to log the Exception as well. But since its not the end of the world level warn is completely acceptable, as the code below will just use default config in care of error.

So all is left to do is to call this new method from our init, compile and test.

Full Example

 1 using NLog;
 2 using System;
 3 using System.IO;
 4 using Torch;
 5 using Torch.API;
 6 
 7 namespace TestPlugin {
 8 
 9     public class TestPlugin : TorchPluginBase {
10 
11         public static readonly Logger Log = LogManager.GetCurrentClassLogger();
12 
13         private Persistent<TestConfig> _config;
14         public TestConfig Config => _config?.Data;
15 
16         public override void Init(ITorchBase torch) {
17             base.Init(torch);
18 
19             SetupConfig();
20         }
21 
22         private void SetupConfig() {
23 
24             var configFile = Path.Combine(StoragePath, "TestConfig.cfg");
25 
26             try {
27 
28                 _config = Persistent<TestConfig>.Load(configFile);
29 
30             } catch (Exception e) {
31                 Log.Warn(e);
32             }
33 
34             if (_config?.Data == null) {
35 
36                 Log.Info("Create Default Config, because none was found!");
37 
38                 _config = new Persistent<TestConfig>(configFile, new TestConfig());
39                 _config.Save();
40             }
41         }
42     }
43 }

Running the server creates the file which looks like this:

1 <?xml version="1.0" encoding="utf-8"?>
2 <TestConfig xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3   <Username>root</Username>
4   <Password />
5   <AuthToken>0</AuthToken>
6   <PreferBulkChanges>true</PreferBulkChanges>
7 </TestConfig>

Changes to the file require a server restart. So you either have to write additional code that reloads the config or have an UI so that administrators can change settings while server is running.

Building an UI

Now that we have config we can load and save we can create create a suitable UI for our Plugin.

For that we have to do 3 things:

  1. Create our xaml file which defines the controls
  2. Add the UI to our Plugin
  3. Set up the proper Bindings

Create a XAML File

Basically this is where you Build your UserControl using WPF. Since this is not a WPF tutorial we keep is short.

There are two things to keep in mind:

  • Often people find themselves with a broken UI when trying to bind Checkboxes. You have to bind isChecked as it is the property. Binding checked is the event and will break the UI.
  • In order for it to work correctly it is mandatory that the Name used in the Binding is exactly the same as in your config. You best copy them over.

This a tiny example of how our config from the previous tutorial would look like.

 1 <UserControl x:Class="TestPlugin.TestControl"
 2       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 4       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
 5       xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
 6       xmlns:local="clr-namespace:TestPlugin"
 7       mc:Ignorable="d" 
 8       d:DesignHeight="450" d:DesignWidth="800">
 9 
10     <Grid>
11 
12         <Grid.ColumnDefinitions>
13             <ColumnDefinition Width="Auto" SharedSizeGroup="Labels"/>
14             <ColumnDefinition Width="*"/>
15             <ColumnDefinition Width="Auto" SharedSizeGroup="Buttons"/>
16         </Grid.ColumnDefinitions>
17         <Grid.RowDefinitions>
18             <RowDefinition Height="Auto"/>
19             <RowDefinition Height="Auto"/>
20             <RowDefinition Height="Auto"/>
21             <RowDefinition Height="Auto"/>
22             <RowDefinition Height="Auto"/>
23             <RowDefinition Height="Auto"/>
24         </Grid.RowDefinitions>
25 
26         <TextBlock Grid.Column="0" Grid.Row ="0" VerticalAlignment="Center" Text="Test Plugin" FontWeight="Bold" FontSize="16" Grid.ColumnSpan="2" Margin="5"/>
27 
28         <TextBlock Grid.Column="0" Grid.Row ="1" VerticalAlignment="Center" Text="Username" Margin="5"/>
29         <TextBox Name="Username" Grid.Column="1" Grid.Row ="1" Grid.ColumnSpan="2" Margin="5" Text="{Binding Username}"/>
30 
31         <TextBlock Grid.Column="0" Grid.Row ="2" VerticalAlignment="Center" Text="Password" Margin="5"/>
32         <TextBox Name="Password" Grid.Column="1" Grid.Row ="2" Grid.ColumnSpan="2" Margin="5" Text="{Binding Password}"/>
33 
34         <TextBlock Grid.Column="0" Grid.Row ="3" VerticalAlignment="Center" Text="Auth Token" Margin="5"/>
35         <TextBox Name="AuthToken" Grid.Column="1" Grid.Row ="3" Grid.ColumnSpan="2" Margin="5" Text="{Binding AuthToken}"/>
36         
37         <TextBlock Grid.Column="0" Grid.Row ="4" VerticalAlignment="Center" Text="Prefer bulk changes" Margin="5"/>
38         <CheckBox Name="PreferBulkChanges" Grid.Column="1" Grid.Row ="4" Grid.ColumnSpan="2" Margin="5" IsChecked="{Binding PreferBulkChanges}"/>
39 
40         <Button Grid.Column="2" Grid.Row="5" Content="Save Config" Margin="5" Click="SaveButton_OnClick"></Button>
41     </Grid>
42 </UserControl>

Note: this is for the sake of demonstration only. User-Credentials should always be handled in a more secure way.

Creating the Binding to our Plugin

When creating a new UserControl Visual Studio will always create a xaml.cs file you can use to interact with your UI. Since we have a SaveButton we want it to do something. Currently its bound to the SaveButton_OnClick Method.

 1 using System.Windows;
 2 using System.Windows.Controls;
 3 
 4 namespace TestPlugin {
 5 
 6     public partial class TestControl : UserControl {
 7 
 8         private TestPlugin Plugin { get; }
 9 
10         private TestControl() {
11             InitializeComponent();
12         }
13 
14         public TestControl(TestPlugin plugin) : this() {
15             Plugin = plugin;
16             DataContext = plugin.Config;
17         }
18 
19         private void SaveButton_OnClick(object sender, RoutedEventArgs e) {
20             Plugin.Save();
21         }
22     }
23 }

We do two things here:

  1. We define a Constructor that takes our Plugin as parameter.
    • We use it to ask it kindly to save settings whenever the SaveButton is pressed.
  2. We Set the UserControls DataContext to our Config. So the Properties of our Config are now being altered whenever the user changes something in the UI.

By the way: Due to the datatype of our properties WPF is able to parse the input itself. So if someone were to enter something non integer in an integer only field it would handle that so you don't have to.

Add the UI to our Plugin

Now with the WPF Stuff out of the way all that is left to do is to instantiate our view and make our Plugin able to use it.

For that we have to let our Plugin implement IWpfPlugin and implement the getControl() Method.

This is actually quite easy:

1     public class TestPlugin : TorchPluginBase, IWpfPlugin {
2 
3         public static readonly Logger Log = LogManager.GetCurrentClassLogger();
4 
5         private TestControl _control;
6         public UserControl GetControl() => _control ?? (_control = new TestControl(this));

All that is left now is to add the Save Method to our plugin.

1         public void Save() {
2             try {
3                 _config.Save();
4                 Log.Info("Configuration Saved.");
5             } catch (IOException e) {
6                 Log.Warn(e, "Configuration failed to save");
7             }
8         }

Compile and Test

Ui testplugin example.png

Looking good, and hitting the save button indeed changes the XMLs content.

Be careful though. The save button just does exactly that. Save to the XML. The changes were performed in memory at the moment the user entered them. So the runtime already has full access to the new changes.

How to Access Game Objects

Similar to the Space Engineers ModAPI you have a few ways to interact with your world.

  • One of which is MyAPIGateway which offers access to the ModAPI
  • Also known from Modding you can use MyVisualScriptLogicProvider for events and interactions with players.
  • MySession.Static is something you cannot use from the Mod API but allows you to access basically everything in the session.
  • MyEntities is a way to directly interact with the entities in your world.

Its recommended to just Add one after an other. If the DLL references are added Visual Studio is able to tell you which namespaces you need to include and from that point its just checking which Method is there and start using them.

However the documentation of Keens methods is not that great so you often either have to trial and error or look at the games code in order to figure out how it works.

Keeping Track of the SessionState

Now that you know how to access things in your Session we should look into when you can do that.

Trying to interact with anything while the game is not ready yet will just end you up with a lot of Exceptions. To prevent that you can keep track of the session. For that Torch has a SessionManager where you can register your Plugin as a listener to Session Changes.

Adding needed imports

You can Ask Torch directly for its managers. However make sure to manually import

1 using Torch.API.Managers;

unfortunately Visual Studio doesn't seem to get it resolved by itself correctly.

Change our Plugin

We now change our Plugins Init to listen for Session Changes.

 1         public override void Init(ITorchBase torch) {
 2             base.Init(torch);
 3 
 4             SetupConfig();
 5 
 6             var sessionManager = Torch.Managers.GetManager<TorchSessionManager>();
 7             if (sessionManager != null)
 8                 sessionManager.SessionStateChanged += SessionChanged;
 9             else
10                 Log.Warn("No session manager loaded!");
11         }
12 
13         private void SessionChanged(ITorchSession session, TorchSessionState newState) {
14 
15             Log.Info("Session-State is now " + newState);
16 
17         }

The TorchSessionState is an enumeration so you can easily use a switch case to pick the session states you are most interested in.

Also this will be useful later when we start working with patches.

There is no real need to unregister your plugin from receiving session state changes. After the server is stopped it has to restart the whole program to be started again. Which torch handles by itself.

Compile and Test

22:29:28.4847 [INFO]   Keen:    Experimental mode: Yes
22:29:28.4847 [INFO]   Keen:    Experimental mode reason: ExperimentalMode, EnableIngameScripts
22:29:39.3968 [INFO]   TorchSessionManager: Starting new torch session for Earth, Mars and Moons
22:29:39.3968 [INFO]   TestPlugin: Session-State is now Loading
22:29:39.4358 [INFO]   Keen:       MyLights preallocated lights cache created.
22:29:39.4358 [INFO]   Keen:    MyLights initialized.

As you can see as soon as the SessionManager declares its loading our plugin is informed and sends off its log.

22:29:47.3037 [INFO]   CommandManager: Registering command 'stop.cancel'
22:29:47.3037 [INFO]   CommandManager: Registering command 'save'
22:29:47.3037 [INFO]   CommandManager: Registering command 'uptime'
22:29:47.3037 [INFO]   TorchSessionManager: Loaded torch session for Earth, Mars and Moons
22:29:47.3037 [INFO]   TestPlugin: Session-State is now Loaded
22:29:47.3107 [INFO]   CommandManager: Registering command 'torch.test'
22:29:47.3107 [INFO]   CommandManager: Registering command 'torch.testWithCommands'
22:29:47.3107 [INFO]   CommandManager: Registering command 'whitelist.on'

Here we see the Session is Loaded and ready for us to be worked with. However the CommandManager is not quite ready doing it stuff.

22:29:48.4378 [INFO]   Keen: Game ready... 

And the Game ready message appears about a second later. Many plugin developers have just a tiny delay in their plugins to start doing things after game is ready. But if that is necessary or not is completely up to what you need to do.

22:33:49.4699 [INFO]   Torch: Stopping server.
22:33:49.4699 [WARN]   Torch: Failed to wait for the game to be stopped
22:33:49.4699 [INFO]   Torch: Server stopped.
22:33:49.4879 [INFO]   Keen: Exiting..
22:33:49.4879 [INFO]   TorchSessionManager: Unloading torch session for Earth, Mars and Moons
22:33:49.4879 [INFO]   TestPlugin: Session-State is now Unloading
22:33:49.4879 [INFO]   Keen: TORCH MOD: Unregistering mod communication.
22:33:49.4879 [INFO]   Keen: TORCH MOD: INFO: Communication thread shut down successfully! THIS IS NOT AN ERROR

Once again right after the SessionManager declares its now unloading our Plugin got word from it.

And so it is when its finally unloaded

22:34:17.5526 [INFO]   Keen: Shutting down server...
22:34:17.6276 [INFO]   Keen: Done
22:34:17.9695 [INFO]   TorchSessionManager: Unloaded torch session for Earth, Mars and Moons
22:34:17.9695 [INFO]   TestPlugin: Session-State is now Unloaded
22:34:17.9695 [INFO]   Keen: TORCH MOD: Unregistering mod communication.

Attaching your plugin to the games update cycle

From the ModAPI you may know that you can cause your Mod to update every tick, every 10 ticks or every 100 ticks.

For Torch Plugins you can do the same. The TorchPluginBase has an Update Method that by default does nothing. But if you wish you can do something in it.

What is a tick?

1 tick = 1/60 of a second or 16.666 ms.

To maintain a simulation speed of 1.0 it is mandatory that all of the games updates are done within these 16.666 ms. If updating takes longer than that the server gets less ticks done in a second and therefore the simulation speed decreases.

Implement the Update Method

1         public override void Update() {
2 
3             Log.Info("Update");
4         }

What you need to do is completely up to you however. Just as a note: The Server and your hard drive doesn't really like 60 Log Messages a second so try not to do that :-)

The update method is being called every tick from the moment the Space Engineers Session is loaded:

22:37:45.4351 [INFO]   Keen:    Session loaded
22:37:45.4851 [INFO]   Keen: Plugin Init: Torch.Server.TorchServer
22:37:45.5411 [INFO]   Keen: Server PolicyResponse (1)
22:37:46.2946 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.2946 [INFO]   Torch: Starting server watchdog.
22:37:46.3886 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.4106 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.4496 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.4785 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.4785 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.4835 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.5045 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.5175 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.5355 [INFO]   Keen: Game ready... 
22:37:46.5355 [INFO]   TestPlugin.TestPlugin: Update
22:37:46.5535 [INFO]   TestPlugin.TestPlugin: Update

Update and Simulation Speed

Your Update Method is completely dependent on the simulation speed of the server. If the Simulation speed is 1.0 your Update Method is called 60 times a second. If its lower than that, for example 0.5 your Method will only be called 30 times a second.

Your Plugin can be the cause for bad simspeed. Since your plugin is in the games update cycle the game thread will call your Update method and only continue once you are finished.

So you should not implement:

1         public override void Update() {
2 
3             Thread.Sleep(100);
4             Log.Info("Update");
5         }

Because it will drop your Simulation speed from 1.0 to 0.17 and effectively making the game unplayable.

Try to only run as often as really needed. If you are fine with running just once a second, or once every 100 seconds you can for example use a StopWatch or a simple invocation counter to skip updates and only do something if really necessary.

Also if possible you could load work off to async tasks and invoke the game thread using

1             MyAPIGateway.Utilities.InvokeOnGameThread(() => {
2                 //DO STUFF
3             });

To get it updated.

Patching Methods

In Space Engineers ModAPI you have MyGameLogicComponents at your disposal. They are used to listen to invocations of certain but not all methods of the annotated ObjectBuilder. Here is an Example for a Medical Room:

 1 [MyEntityComponentDescriptor(typeof(MyObjectBuilder_MedicalRoom), useEntityUpdate: false)]
 2 public class Medbay : MyGameLogicComponent {
 3 
 4     public override void Init(MyObjectBuilder_EntityBase objectBuilder) {
 5 
 6         base.Init(objectBuilder);
 7 
 8         NeedsUpdate = MyEntityUpdateEnum.EACH_FRAME;
 9     }
10 
11     public override void UpdateAfterSimulation() {
12         //Some Code
13     }
14 
15     public override void Close() {
16         base.Close();
17     }
18 }

However. GameLogicComponents dont exist in torch. Since we got Patching we don't have to.

What is patching?

Patching means you alter the behavior of the server by adding your own logic to existing methods of the game, preventing them from being executed or completely replacing them.

In torch this is the job of the PatchManager which basically behaves like a method proxy. You can select any Method you like, and add it to the PatchManager. Every time this method is then called your own method will also be invoked.

You are free to choose to Prefix, or Suffix a Method, which allows you to become active either before, or after the games code has executed.

Preparing your Plugin for Patching

First we need to grab an instance of the PatchManager to and aquire a PatchContext. The PatchContext works as an transaction. You can attach or detach several patching rules to it and commit these changes to the PatchManager as soon as you are ready.

This shows how to grab the PatchManager from torches Managers:

 1         private PatchManager patchManager;
 2         private PatchContext ctx;
 3 
 4         public override void Init(ITorchBase torch) {
 5             base.Init(torch);
 6 
 7             patchManager = Torch.Managers.GetManager<PatchManager>();
 8             if (patchManager != null) {
 9 
10                 if (ctx == null)
11                     ctx = patchManager.AcquireContext();
12 
13             } else {
14                 Log.Warn("No patch manager loaded!");
15             }
16         }

Creating your patch class

Although it is not necessary it is recommended to create your Patch in a dedicated class called <EntityYouWantToPatch>Patch.

This makes the different parts of your plugin more distinguishable from an other and allows you to easily add more methods to patch the same class in the future.

Here is one example on how to create a Patch for the Update10 Method of a MyLargeTurretBase

 1 using Sandbox.Game.Weapons;
 2 using System;
 3 using System.Reflection;
 4 
 5 namespace TestPlugin {
 6 
 7     class MyLargeTurretBasePatch {
 8 
 9         internal static readonly MethodInfo update =
10             typeof(MyLargeTurretBase).GetMethod(nameof(MyLargeTurretBase.UpdateAfterSimulation10), BindingFlags.Instance | BindingFlags.Public) ??
11             throw new Exception("Failed to find patch method");
12 
13         internal static readonly MethodInfo updatePatch =
14             typeof(MyLargeTurretBasePatch).GetMethod(nameof(TestPatchMethod), BindingFlags.Static | BindingFlags.Public) ??
15             throw new Exception("Failed to find patch method");
16 
17         public static void TestPatchMethod(MyLargeTurretBase __instance) {
18             __instance.Enabled = !__instance.Enabled;
19         }
20     }
21 }

First we aquire the MethodInfo using reflection of the method we would like to patch and the method you want to patch it with. Also there is a small TestPatchMethod, which we use for testing purposes. Basically every 10 Ticks the enabled state of the turret should change in this case.

return types and parameters of your patch method will be explained later.

If you only want to patch public instance methods, you can simplify code to:

 1     class MyLargeTurretBasePatch {
 2 
 3         [ReflectedMethodInfo(typeof(MyLargeTurretBase), "Update10")]
 4         private static readonly MethodInfo update;
 5 
 6         [ReflectedMethodInfo(typeof(MyLargeTurretBasePatch), "TestPatchMethod")]
 7         private static readonly MethodInfo updatePatch;
 8 
 9         public static void TestPatchMethod(MyLargeTurretBase __instance) {
10             __instance.Enabled = !__instance.Enabled;
11         }
12     }

The ReflectedMethodInfo annotation is used by torch to automatically get the MethodInfo for you. However this only works with very specific methods so unless you have figured out the conditions it works at you are best off using the manual approach on getting your MethodInfos.

The example above is just for demonstration and does not really work

Applying your Patch

Now your Patch is prepared we need to add it to the PatchContext. For that we create a Patch Method in our Patch Class that takes care of that. Since we are not exposing the Method info our Plugin cannot do that for us.

 1         public static void Patch(PatchContext ctx) {
 2 
 3             ReflectedManager.Process(typeof(MyLargeTurretBasePatch));
 4 
 5             try {
 6 
 7                 ctx.GetPattern(update).Prefixes.Add(updatePatch);
 8 
 9                 Log.Info("Patching Successful MyLargeTurretBase!");
10 
11             } catch (Exception e) {
12                 Log.Error(e, "Patching failed!");
13             }
14         }

ReflectedManager.Process() manages the Reflection for us. You dont need to do that if you are not using the ReflectedMethod annotation, because that's basically what its used for. Finding the fields with the ReflectedMethod annotation and getting the Method info.

Then we apply our patch method as prefix to the method we want to patch. Which means this Method is run before the actual method is executed. Alternatively you could use Suffixes to execute your method after the original has run.

In Our Plugin we now call our Patch Method and commit them to the PatchManager. Since it makes only sense to patch this particular Method after a session is available and the game is running we wait until the session is finally loaded before applying the patch.

 1         public override void Init(ITorchBase torch) {
 2             base.Init(torch);
 3 
 4             SetupConfig();
 5 
 6             var sessionManager = Torch.Managers.GetManager<TorchSessionManager>();
 7             if (sessionManager != null)
 8                 sessionManager.SessionStateChanged += SessionChanged;
 9             else
10                 Log.Warn("No session manager loaded!");
11 
12             patchManager = Torch.Managers.GetManager<PatchManager>();
13             if (patchManager != null) {
14 
15                 if (ctx == null)
16                     ctx = patchManager.AcquireContext();
17 
18             } else {
19                 Log.Warn("No patch manager loaded!");
20             }
21         }
22 
23         private void SessionChanged(ITorchSession session, TorchSessionState newState) {
24 
25             if (newState == TorchSessionState.Loaded) {
26 
27                 MyLargeTurretBasePatch.Patch(ctx);
28                 patchManager.Commit();
29             }
30 
31             Log.Info("Session-State is now " + newState);
32         }

And now we are done.

Overview of Patch-Types

  • Prefix
    • Callback that runs at beginning of a method. If you return false the method won't execute.
  • Suffix
    • Callback that runs at the end of a method.
  • Transpiler
    • transformer run on the raw IL (Intermediate Language) of the method body before prefixes/suffixes get added
  • PostTranspiler
    • transformer run after the prefixes/suffixes get added

An example for Transpilers can be found [ https://github.com/Equinox-/FastReplicate/blob/master/FastReplicate/Patch.cs#L146 | here ]

Compile and Test

To test the code lets add a bit of logging to the Patch:

1         public static void TestPatchMethod(MyLargeTurretBase __instance) {
2             __instance.Enabled = !__instance.Enabled;
3             Log.Info("Patch __instance enabled " + __instance.Enabled);
4         }

So we can see on the console if it works or not.

22:28:17.2505 [INFO]   Torch.Session.TorchSessionManager: Loaded torch session for Earth, Mars and Moons
22:28:17.2505 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patching Successful MyLargeTurretBase !
22:28:17.2505 [INFO]   Torch.Managers.PatchManager.PatchManager: Patching begins...
22:28:17.2505 [INFO]   Torch.Managers.PatchManager.PatchManager: Patched 1/1.  (100%)
22:28:17.2505 [INFO]   Torch.Managers.PatchManager.PatchManager: Patching done
22:28:17.2505 [INFO]   TestPlugin.TestPlugin: Session-State is now Loaded

Okay we see the PatchManager patched 1 Method. great and our Patching Successful LogEntry appears.

As soon as the Session is Loaded we get

22:28:17.2625 [INFO]   Keen:    Session loaded
22:28:17.3075 [INFO]   Keen: Plugin Init: Torch.Server.TorchServer
22:28:17.3635 [INFO]   Keen: Server PolicyResponse (1)
22:28:18.2275 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.2275 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.3596 [INFO]   Torch: Starting server watchdog.
22:28:18.4665 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.4665 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.4665 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.5245 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.5765 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.6415 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.6415 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.6415 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled False
22:28:18.6415 [INFO]   Keen: Game ready... 
22:28:18.6735 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True
22:28:18.6735 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True
22:28:18.6735 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True
22:28:18.6735 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True
22:28:18.6735 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True
22:28:18.6845 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True
22:28:18.7064 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True
22:28:18.7444 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True
22:28:18.7614 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True
22:28:18.7614 [INFO]   TestPlugin.MyLargeTurretBasePatch: Patch __instance enabled True

This Method is now called for every Turret on the Server. So first all of them were disabled. Then all of them were enabled and so on and so forth.

So our Patch works.

Parameters

When Patching you can automatically aquire all parameters that would go into the patched method by adding them to your own methods signature. Make sure that parameter type and name must be exactly the same in order to work.

Special Parameters

There are a few reserved special parameters to get information on the object you are dealing with.

  • __instance
    • Will provide you with the instance of the object the patched method is called on.
    • You should always cross check if __instance is of the correct type, because the type in the method signature cannot be trusted. Calling methods assuming it is the type you expect, but in the end is not can crash your server with NREs and other RuntimeExceptions.
  • __result
    • Mostly useful if you suffix a method. And will contain the result of the real method. Must have the same parameter type as the return type of the patched method.
  • __prefixSkipped
    • Must be a bool value that returns true if the prefix was skipped. Pretty self explainatory.
  • __local
    • Prefix to a local variable name. Exact usage currently unclear.

Some examples on how to use it can be found | here and | here.

AlterParameters

If you want to alter the Parameters going into the patched Method you can get them by ref and just change them as you see fit.

Return Types

Your Method can either have a void, or a bool value. Having it void is the equivalent to the method returning true.

If the Method however returns false and you are prefixing, the patched method will not be called.

1         public static bool TestPatchMethod(MyLargeTurretBase __instance) {
2             __instance.Enabled = !__instance.Enabled;
3 
4             return false;
5         }

In this example we change the state of our object and then just let it return false. Which means the method that was supposed to be called will not be.

This can potentially screw things up on your server, so be careful which methods you disable.

The Concealment Plugin for example takes advantage of this method to disable grid updates when no player is close by.

Things to be aware of while patching

You need to be careful when patching, if the method you try to patch is available or not.

A quick example: UpdateAfterSimulation100 is implemented by MyEntity, but not by MyLargeTurretBase.

So if you patch it, you will not patch a turret, but every Entity there is. So your __instance parameter can contain everything. From refineries to player characters. Hence why you should check for the correct type if your parameter even if one specific type is in the method signature. And always make sure if the method is actually implemented for the class you like to patch.

Full Example of Patch

 1 using NLog;
 2 using Sandbox.Game.Weapons;
 3 using System;
 4 using System.Reflection;
 5 using Torch.Managers.PatchManager;
 6 using Torch.Utils;
 7 
 8 namespace TestPlugin {
 9 
10     class MyLargeTurretBasePatch {
11 
12         public static readonly Logger Log = LogManager.GetCurrentClassLogger();
13 
14         internal static readonly MethodInfo update =
15             typeof(MyLargeTurretBase).GetMethod(nameof(MyLargeTurretBase.UpdateAfterSimulation10), BindingFlags.Instance | BindingFlags.Public) ??
16             throw new Exception("Failed to find patch method");
17 
18         internal static readonly MethodInfo updatePatch =
19             typeof(MyLargeTurretBasePatch).GetMethod(nameof(TestPatchMethod), BindingFlags.Static | BindingFlags.Public) ??
20             throw new Exception("Failed to find patch method");
21 
22         public static void Patch(PatchContext ctx) {
23 
24             ReflectedManager.Process(typeof(MyLargeTurretBasePatch));
25 
26             try {
27 
28                 ctx.GetPattern(update).Prefixes.Add(updatePatch);
29 
30                 Log.Info("Patching Successful MyLargeTurretBase!");
31 
32             } catch (Exception e) {
33                 Log.Error(e, "Patching failed!");
34             }
35         }
36 
37         public static void TestPatchMethod(MyLargeTurretBase __instance) {
38             __instance.Enabled = !__instance.Enabled;
39         }
40     }
41 }

Logger was removed as it tends to lag the servers output if called too often.