Plugin Development Tutorials

From Torch Wiki
Jump to: navigation, search

Creating plugins for torch is very easy. For most of the plugins you only need a few classes, and unlike the ModAPI for Keen Mods you are not limited by any kind of whitelist that interferes with your code, or restricts access to runtime objects. Effectively you can do everything you want and change any behavior of the server.

The following sections give an simple overview of how to start development and creating some basic functionalities to your plugin. Of course this compilation is no step by step guide to create the plugin you wish to create, but offers the basic ideas of what you may/need to do.


Getting Started

First of all you need an IDE for developing because it will make your life a whole lot easier. Basically you can use any IDE you wish for c# .net development. This tutorial however uses | Microsoft Visual Studio as the IDE. The Community Edition is free to use, and after about a month only requires you to sign into a Microsoft account. Any Hotmail, Live etc. account will do. Chances are high you already have one.

Open Visual Studio and create a new Project. Depending on what kind of Plugin you wish to create you need to choose a different type of Project. Of course you can decide later, but knowing in advance has the advantage that Visual Stuido will add the needed references to standard libraries you need for creating and compiling the UI in advance so that you don't have to deal with that yourselves.

Creating a Plugin with UI (Choose if you are not sure yet)

If you are unsure if you want to create an UI for the User you can still choose this option. Even if you end up not creating an UI your plugin wont be hurt by it. So we Recommend using this.

For that you create c# WPF-App (.net Framework) and use .net Framework of version 4.6.0 or later for development (4.7.2 is recommended).

Vs create new project.png

However: This whole step is just needed to get the needed references into your project, as it is a bit tedious to do it yourselves. So after creating the project, change its type to ClassLibrary. Otherwise upon compiling it will complain about you not having a suitable main Method.

Creating a Plugin without UI

If you are absolutely sure you don't need an UI and are never planning to add one a simple ClassLibrary will do.

For that you create c# ClassLibrary (.net Framework) and use .net Framework of version 4.6.0 or later for development (4.7.2 is recommended).

Older versions may cause issues with any referenced DLLs that were compiled using a newer version of .net.

You don't have the required .net Framework?

Usually that should not be the case as you are most likely have developed before. But in case you usually program with a different language most of the time (for example java) you can download it manually | here

Managing Dependencies

Now you created an empty Project. For the sake of simplicity it is assumed you started with a WPF-App and therefore have a few default classes in your project. For now you can delete those as they are not needed.

To create plugins you have to implement against interfaces defined by torch. So there are a few References you need to import.

For Basic Plugin Development you need:

  • torch.dll
  • torch.API.dll
  • torch.Server.exe

You find these DLLs in your torch servers folder. Its recommended to use these and not to copy them anywhere, as they are automatically updated if a new torch version happens to come out.

For Logging its also recommended to include:

  • NLog.dll

into your project.

In the DedicatedServer64 Folder next to your torch.Server.exe you find the DLLs the game uses. You want to grab

  • Sandbox.*.dll
  • SpaceEngineers.*.dll
  • VRage.*.dll

However most of the VRage.*.dlls you will never use. It doesn't hurt adding them, but after you created a few plugins you have a pretty good feeling on which DLLs you need and which you don't. Just be adviced. VRage.Native.dll cannot be imported anyway so don't bother trying.

What have you done so far?

Now you should have archived the following goals:

  • Installed an IDE (if you hadn't already)
  • Installed the .net Framework (if you hadn't already)
  • Set up a new Project
  • Added the needed dependencies

So you are all set to create the first plugin.

Writing a Basic Plugin

Now that you have set up your project you can start by adding your Main class and Namespace.

Write the Plugins Code

Your Plugin needs to have a public class that implements TorchPluginBase and there must only be one class that implements that. When torch loads your plugin it checks every class its finds to look for the one that implements it. If there are none or multiple, you will get a log messages upon server start telling you it was unable to load your plugin.

Visual Studio creates new classes without visibility which causes Torch to not see your class and therefore not load it.

Here is a small example:

 1 using NLog;
 2 using Torch;
 3 using Torch.API;
 5 namespace TestPlugin {
 7     public class TestPlugin : TorchPluginBase {
 9         public static readonly Logger Log = LogManager.GetCurrentClassLogger();
11         /// <inheritdoc />
12         public override void Init(ITorchBase torch) {
13             base.Init(torch);
14         }
15     }
16 }

The Init() Method is called by Torch's PluginManager to initialize it. In this Method you do everything you need to do to get your plugin up and running.

However: This Method will be called before the game is even started. So you cannot interact with anything else than Torch itself.

Also Worth noting: Unlike in-game scripts or mods you write for Space Engineers, all your code is compiled by yourself. So you are not limited in any functions. As long as it compiles, it will work.

Test your Plugin

To see if your Plugin works, lets just add a small Log Message to the Init Method and compile it.

1         public override void Init(ITorchBase torch) {
2             base.Init(torch);
4             Log.Info("This is a Test if it works!");
5         }

After compiling you will find a bin folder in your projects directory. In that you find (depending of how you built it) a DLL file. This is your plugin.

Setup a Plugin zip

In order for Torch to load your Plugin you have to put the newly created .dll file into a zip-compressed folder. You can use Windows own way of doing that, or use a third party program like 7Zip or WinRar to do so.

Along with the .dll you have to add a manifest.xml which Looks like this:

1 <?xml version="1.0"?>
2 <PluginManifest xmlns:xsd="" xmlns:xsi="">
3   <Name>TestPlugin</Name>
4   <Guid>00000000-0000-0000-0000-0000000000</Guid>
5   <Repository>None</Repository>
6   <Version>v1.0.0</Version>
7 </PluginManifest>
  • Name
    • The Name is the name of your Plugin, it will be needed for Publication later on, cannot be changed once your Plugin is published and is also displayed in the Torch UI.
    • Assuming you have already settled on a good name just type it in there.
  • Guid
    • The Guid is a Unique plugin identifier, torch uses to tell plugins apart.
    • By that you can have multiple plugins with the same name. There are online generator that generate an GUID for you. As an example you can use: [1]
  • Repository
    • Repository is deprecated and can be left on "None". It was used to track plugin changes on GitHub, but since there is a website for that now it has no use.
  • Version
    • Version is used to distinguish between newer and older versions of your plugin. Versions should be formatted according to semantic versioning.
    • Torch uses the version number to decide whether to update a plugin to a newer version or not.

Now you should have a zip file that looks like this: -> TestPlugin.dll -> manifest.xml

Install your Plugin

You install your own plugin exactly the same as any other plugin. Just follow the instructions on [Plugins]

Dont Worry, you dont need to do that every time, once installed you just have to replace your .dll file in that zip. This should just be a drag and drop action after compiling. So its pretty easily done.

Start the Server

When starting you should find the following entries in your log:

18:27:14.7832 [INFO]   PluginManager: Checking for plugin updates...
18:27:14.9051 [INFO]   PluginManager: Updated 0 plugins.
18:27:14.9051 [INFO]   PluginManager: Loading plugins...
18:27:14.9241 [INFO]   PluginManager: Loading plugin at TestPlugin.TestPlugin
18:27:14.9241 [INFO]   PluginManager: Loading plugin 'TestPlugin' (v1.0.0)
18:27:14.9371 [INFO]   TestPlugin: This is a Test if it works!
18:27:14.9371 [INFO]   PluginManager: Loaded 1 plugins.

We see the PluginManager loaded 1 plugin, which is the one you just created. And we see our Log Message. It works!

Apply Changes to your Plugin

When applying changes to your plugin while developing you always have to restart your server. This can be a bit tedious as saving and loading takes some time.

It is recommended to use the | ALE Restart Watchdog plugin. You configure it to restart the server automatically after 1 second. So whenever you hit the stop button, type !stop in your console, or press the X on the UI this plugin will immediately restart your server. Saving precious time that would otherwise be wasted on unloading and saving the world.

If you need to save however you have to trigger it manually via !save command and wait for it to be completed.

Creating Commands

One of the most basic used 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;
3 namespace TestPlugin {
5     public class TestCommands : CommandModule {
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;
 5 namespace TestPlugin {
 7     public class TestCommands : CommandModule {
 9         [Command("test", "This is a Test Command.")]
10         [Permission(MyPromoteLevel.Moderator)]
11         public void Test() {
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.


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 {
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;
 5 namespace TestPlugin {
 7     [Category("torch")]
 8     public class TestCommands : CommandModule {
10         public TestPlugin Plugin => (TestPlugin) Context.Plugin;
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         }
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;
3 namespace TestPlugin {
4     public class TestConfig : ViewModel {
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;
 3 namespace TestPlugin {
 4     public class TestConfig : ViewModel {
 6         private string _Username = "root";
 7         private string _Password = "";
 8         private int _AuthToken = 0;
 9         private bool _PreferBulkChanges = true;
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;
3 namespace TestPlugin {
4     public class TestConfig : ViewModel {
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() {
 3             var configFile = Path.Combine(StoragePath, "TestConfig.cfg");
 5             try {
 7                 _config = Persistent<TestConfig>.Load(configFile);
 9             } catch (Exception e) {
10                 Log.Warn(e);
11             }
13             if (_config?.Data == null) {
15                 Log.Info("Create Default Config, because none was found!");
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;
 7 namespace TestPlugin {
 9     public class TestPlugin : TorchPluginBase {
11         public static readonly Logger Log = LogManager.GetCurrentClassLogger();
13         private Persistent<TestConfig> _config;
14         public TestConfig Config => _config?.Data;
16         public override void Init(ITorchBase torch) {
17             base.Init(torch);
19             SetupConfig();
20         }
22         private void SetupConfig() {
24             var configFile = Path.Combine(StoragePath, "TestConfig.cfg");
26             try {
28                 _config = Persistent<TestConfig>.Load(configFile);
30             } catch (Exception e) {
31                 Log.Warn(e);
32             }
34             if (_config?.Data == null) {
36                 Log.Info("Create Default Config, because none was found!");
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="" xmlns:xsi="">
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=""
 3       xmlns:x=""
 4       xmlns:mc="" 
 5       xmlns:d="" 
 6       xmlns:local="clr-namespace:TestPlugin"
 7       mc:Ignorable="d" 
 8       d:DesignHeight="450" d:DesignWidth="800">
10     <Grid>
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>
26         <TextBlock Grid.Column="0" Grid.Row ="0" VerticalAlignment="Center" Text="Test Plugin" FontWeight="Bold" FontSize="16" Grid.ColumnSpan="2" Margin="5"/>
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}"/>
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}"/>
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}"/>
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}"/>
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;
 4 namespace TestPlugin {
 6     public partial class TestControl : UserControl {
 8         private TestPlugin Plugin { get; }
10         private TestControl() {
11             InitializeComponent();
12         }
14         public TestControl(TestPlugin plugin) : this() {
15             Plugin = plugin;
16             DataContext = plugin.Config;
17         }
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 {
3         public static readonly Logger Log = LogManager.GetCurrentClassLogger();
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);
 4             SetupConfig();
 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         }
13         private void SessionChanged(ITorchSession session, TorchSessionState newState) {
15             Log.Info("Session-State is now " + newState);
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() {
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() {
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 {
 4     public override void Init(MyObjectBuilder_EntityBase objectBuilder) {
 6         base.Init(objectBuilder);
 8         NeedsUpdate = MyEntityUpdateEnum.EACH_FRAME;
 9     }
11     public override void UpdateAfterSimulation() {
12         //Some Code
13     }
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;
 4         public override void Init(ITorchBase torch) {
 5             base.Init(torch);
 7             patchManager = Torch.Managers.GetManager<PatchManager>();
 8             if (patchManager != null) {
10                 if (ctx == null)
11                     ctx = patchManager.AcquireContext();
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;
 5 namespace TestPlugin {
 7     class MyLargeTurretBasePatch {
 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");
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");
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 {
 3         [ReflectedMethodInfo(typeof(MyLargeTurretBase), "Update10")]
 4         private static readonly MethodInfo update;
 6         [ReflectedMethodInfo(typeof(MyLargeTurretBasePatch), "TestPatchMethod")]
 7         private static readonly MethodInfo updatePatch;
 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) {
 3             ReflectedManager.Process(typeof(MyLargeTurretBasePatch));
 5             try {
 7                 ctx.GetPattern(update).Prefixes.Add(updatePatch);
 9                 Log.Info("Patching Successful MyLargeTurretBase!");
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);
 4             SetupConfig();
 6             var sessionManager = Torch.Managers.GetManager<TorchSessionManager>();
 7             if (sessionManager != null)
 8                 sessionManager.SessionStateChanged += SessionChanged;
 9             else
10                 Log.Warn("No session manager loaded!");
12             patchManager = Torch.Managers.GetManager<PatchManager>();
13             if (patchManager != null) {
15                 if (ctx == null)
16                     ctx = patchManager.AcquireContext();
18             } else {
19                 Log.Warn("No patch manager loaded!");
20             }
21         }
23         private void SessionChanged(ITorchSession session, TorchSessionState newState) {
25             if (newState == TorchSessionState.Loaded) {
27                 MyLargeTurretBasePatch.Patch(ctx);
28                 patchManager.Commit();
29             }
31             Log.Info("Session-State is now " + newState);
32         }

And now we are done.

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.


As shown above, you can get the instance of the Object the original method was called on by having a parameter called __instance. If you do, you will find your instance in there. However 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.

after __instance (when needed) you can just add all other parameters the original Method takes in (in the correct order of course) and have access to it. Some examples can be found | here.

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;
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;
 8 namespace TestPlugin {
10     class MyLargeTurretBasePatch {
12         public static readonly Logger Log = LogManager.GetCurrentClassLogger();
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");
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");
22         public static void Patch(PatchContext ctx) {
24             ReflectedManager.Process(typeof(MyLargeTurretBasePatch));
26             try {
28                 ctx.GetPattern(update).Prefixes.Add(updatePatch);
30                 Log.Info("Patching Successful MyLargeTurretBase!");
32             } catch (Exception e) {
33                 Log.Error(e, "Patching failed!");
34             }
35         }
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.

Publishing Your Plugin

You are done creating your Plugin? You want to share with with the world? Great. This is how you do it:

First you go to the and login to your account. If you don't have one yet, you can create one either by logging in via Github or Patreon, or creating your Account manually.

Review Process (Currently)

Currently, there is a manual Review process by Jimmacle. Quote:

If you're a plugin developer and would like to publish plugins on the website please DM me with your website account name and a link to your plugin's source code so I can review it and give you permission.

The Review process is there to prevent people to do stuff they are not supposed to. However you only need to go through the review once.

It is not mandatory to share your source code with the public. Only Jimmacle needs to see it.

Review Process (In the future)

Since this adds some unpleasant overhead its planned in the future to completely remove the review process. Instead plugin developers get a "trusted" flag if their plugins are okay later on.

How that "trusted" flag can be aquired is still unclear. You can look up details | here

Create your Plugin Page

On the Torch website in the plugin tab you can now hit "Create new Plugin" button. It will transition you to a small form where you can create your Plugins Page.

You have to fill in your plugins GUID, Your plugins name and a description of at least 10 characters. Optionally you can upload a custom logo for your plugin.

Note: Name and GUID must match your plugins manifest.xml exactly it is case sensitive and if not identical you either have to delete and recreate the plugin or change your manifest.xml.

It is recommended to create the Page first with a "Placeholder" description and add the description later. So in case something goes wrong you don't have to write everything again.

Add your Description

First of all Markdown is supported. It is unknown to what extend, but many of the Markdown examples | here seem to work. Not all though. And occasionally the outcome looks a whole lot different then what you would expect.

Add a general description of what your Plugin does, which commands there are and maybe a quick installation guide where you explain what the different settings do. Unless your plugin is completely self explanatory.

This is also a good place to add a link to your github if you wish to share your source with anyone else. Many administrators like to inspect a plugin themselves before using it just to make sure its fine. But thats up to you.

Upload your Plugin

Now you can just go to the torch plugin page and hit the "Upload Release" button. Select your zip file, add a small description of what changed and hit upload.

From that point people can download your plugin and start using it. For support request and advertisement purposes it may be useful to join torches discord server. But that's up to you.

How to figure out how the games code works

Currently there is no current copy of the games code publicly available. So you can only resort to decompiling the games dlls to access the code and find out how different Methods work. However keep in mind that the Keen Software House EULA applies. (Last updated on July 3rd, 2019)

For decompiling there are two results that pop up in google when looking for it.

In both you can basically load the games dll files and look up the classes you need to see.

The decompiled source code of dotPeek is easier to reed. But dnSpy has other advantaged which will come in handy in the next tutorial.

But what tool you use is completely up to you. There are even some with an Visual Studio integration.

How to debug your code

Since you cannot really test any runtime relevant code with just your plugin you have to get a bit more creative.

You could either spent some time downloading torches source code, alter a bit so that it loads your plugin without being in the zip file so you can use Visual Studio for debugging... Which Honestly is a bit annoying to set up.

Or you use the neat little Tool called dnSpy

dnSpy allows you to either to attach to running torch instances, or start torch directly through it. Which allows you to decompile its code, set breakpoints, break on exceptions and what not.

How to set it up

Basically you just download dnSpy from the source listed above, unpack it and you are good to go.

Depending on how you usually run your torch server it may be helpful to run it with administrator privileges as you have access to more information then.

Once you start it (and in my case set the theme to light, because black sucks) you can launch Torch.

Start Debugging

Either Press F5 to launch a software using dnSpy, or ctrl + alt + P to attach to a running process. You can also debug your productive servers that way, however stopping debugging will kill the application that has been debugged. So you better don't want to do that. Also the simulation speed will be much worse while debugging. So keep that in mind.

We assume you start torch for debugging. so you hit F5 (or use) the Menubar Debug -> Start Debugging and get a window that looks a little something like this:

DnSpy assembly selection.png

Of course your language setting may be different.

Just select your torch.server.exe and hit OK.

Setting Breakpoints

Once the server runs you can open the Module Explorer (Ctrl + Alt + U) and look for our Plugin.

DnSpy module explorer.png

There we see our plugins dll loaded.

Double click it and you can explore it using the Assembly Explorer (Ctrl + Alt + L)

DnSpy assembly explorer.png

As an example we open the TestCommands.cs and next to the line numbers we can set a breakpoint via simple click.

DnSpy breakpoints.png

Here you also can see how our decompiled sourcecode looks. Quite similar to what we created before. But they added some tokes, altered enumerations and added default parameters we did not set.

Analysing your Plugin

So lets just enter the command we just set a breakpoint to. You will notice it automatically opens the file where the breakpoint is set. And you can inspect your code now.

  • You can use Local-Variables (Alt + 4)
  • Look at the Stacktrace (Ctrl + Alt + C)
  • Check out your Threads (Ctrl + Alt + H)

You can open all of these windows manually also and arrange them as you need. dnSpy will save your view layout so you only need to do that once.

DnSpy debug lokals.png

DnSpy debug stacktrace.png

Once you are done inspecting you can just continue using (F5) or go step by step through your Program.

These buttons will help you:

DnSpy debug step by step.png

Exception Settings

If your Server dies due to a fatal exception dnSpy will tell you which it was. And also has a possability for you to set up breakpoints when exceptions are thrown.

For that the Exception Tab is used. (Ctrl + Alt + E)

There you can select on which exceptions your Server should halt, so you can look into things.

DnSpy debug exceptions.png

However: The game throws a ton of exceptions even to start up. So it takes a bit fine tuning to find the right exceptions to stop to. If you use your own exceptions in your Plugin it makes it a bit easier.