Skip to content
Nico Borgsmüller edited this page Mar 11, 2022 · 1 revision

Introduction

This API is still in it's first version but we understand the importance of keeping it as stable as possible to avoid breaking the compatibility with other mods, for this reason we are committed to not adding any breaking changes anytime soon. An example for a CSM supported mod can be found in the repository in the examples/SampleExternalMod folder.

There are two ways to provide support for a specific mod. The support can either be implemented into the mod itself or into CSM, although the first method is preferred. Both approaches are very similar, but differences will be mentioned below when necessary.

The Modding API

The CSM modding API provides the following features:

  • Sending and receiving own commands: These are needed to synchronize changes between the games. The CSM protocol is automatically adapted to the registered and enabled mods.
  • Printing messages to the chat
  • Logging messages: CSM provides its own multiplayer-related logging file.
  • Helper classes: Some helper classes like the IgnoreHelper and ReflectionHelper can be used. They will be explained further below.

To synchronize changes, a mod needs to extend the Command class for each data packet. Commands represent the network package, it's what transports the data that we need to send/receive. Each Command then needs a CommandHandler which CSM.API will call automatically if a Command is received and the mod needs to implement in it the necessary logic to replicate this change in the clients side. More details on this will come in the next sections.

Download and import CSM.API

First, start by downloading the latest stable version of CSM.API either from Github and adding the CSM.API.dll file as dependency or adding the CitiesSkylinesMultiplayer.API NuGet package to your project.

Register your CSM extension

To register as a mod with CSM support, you need to provide a manager class. This class needs to extend the Connection class from CSM.API. In the constructor, you need to set different attributes:

  • Name: A unique name of your mod
  • Enabled: If this class is part of CSM, we need to detect if the mod is loaded and enabled. This should then be set to true. If this class is part of the external mod, this can always be true.
  • ModClass: The type of the mod class that implements IUserMod. Must be set if the enabled flag is true.
  • CommandAssemblies: A list of assemblies to search for commands. Only commands in these assemblies are added to the protocol.

Furthermore, the Connection class provides two abstract methods RegisterHandlers and UnregisterHandlers. Those will be called when loading/unloading the map and this external mod is enabled. You can use these to register any handling logic. Note that you should only ever call any CSM API methods after RegisterHandlers and before UnregisterHandlers was called.

Providing your connection class has a few different effects in addition to the detection of commands: The CSM mod will detect it and show your mod as "Supported" on the join and host game screens. Additionally, it will check if the other side also has the mod installed and prevent joining otherwise.

Sending and Receiving Commands

To communicate between the different player clients, you first need to define a command. This command class must extend CommandBase and be located inside an assembly that was added to the CommandAssemblies collection of your mod support class. The class must be annotated with [ProtoContract] from the protobuf-net library. It can contain one or more fields that should be public properties with a public getter and setter. Every field must be annotated with [ProtoMember(x)] were x is a number starting at 1 that is increased for every field.

Example: [ProtoMember(1)] public string test { get; set; }

To send this class to other players, the Command.SendToAll method can be used like so:

Command.SendToAll(new TestCommand { test = "hello" });

It will automatically be serialized and send to the other players. To send the command only to the server or only to the clients, the Command class also provides the SendToServer and SendToClients methods, although you will most likely never need them. To find out if the current game is a server, a client or neither, you can access the Command.CurrentRole field.

To receive messages, a command handler class is required. This class needs to extend CommandHandler<T> where T is the command type that should be handled. You need to implement the method Handle(T) in this class. It will be called once a command of the specific type T was received. If you ever need to access the class instance of a handler class from outside to set any variables for example, you can use Command.GetCommandHandler(Type).

Utilities

The API provides some more utilities for easier handling of the multiplayer experience.

The IgnoreHelper

A common implementation of multiplayer synchronization is to send out a command once a specific method is called and then call this exact method on the other game with the exact same parameters. This can easily be implemented without changing much of the existing code base. But this poses a major problem, if the method is called on the other game, this game will also send out the same packet resulting in a loop.

The IgnoreHelper class can be used to prevent this. Once you receive a command, call IgnoreHelper.Instance.StartIgnore(), then call your method, then call EndIgnore(). Inside your method, wrap the logic of sending out a packet with !IgnoreHelper.Instance.IsIgnored(). This sounds simple, although it is very useful when the original code can't be modified and events are only captured by patching the original methods like is done for the base Cities game in CSM.

Further information can be found on this wiki page: Tracking changes

The ReflectionHelper

This class offers a number of simplifications for using C# Reflection, although they are most likely unnecessary to use for a mod.

The Log class

This class can be used to write messages related to multiplayer into the CSM Log file. For example call Log.Info("Hello"); to write out the text "Hello" with logging level "Info". Messages with level "Debug" are only written when debug logging is enabled in the in-game settings of CSM.

The Chat class

The chat class can be used to display messages in the in-game chat. This is normally the chirper although in can be switched to a separate chat window.

Use Chat.Instance.PrintGameMessage("Hello") or Chat.Instance.PrintChatMessage("name", "message") to print messages to chat.

Distributing your mod with CSM support

The only requirement for your mod is to provide the connection class somewhere in one of your assemblies. It doesn't have to be registered anywhere, it is automatically detected by CSM once your mod is enabled. You only need to bundle the CSM.API.dll with your mod.

Example

Full examples of the important classes might look like the following.

The connection class:

using CSM.API;

namespace SampleExternalMod
{
    class SampleExternalModSupport : Connection
    {
        public SampleExternalModSupport()
        {
            name = "Sample External Mod";
            Enabled = true;
            ModClass = typeof(SampleUserMod);
            CommandAssemblies.Add(typeof(SampleExternalModSupport).Assembly);
        }

        public override void RegisterHandlers() {
          // Enable CSM support
        }

        public override void UnregisterHandlers() {
          // Disable CSM support
        }
    }
}

The command:

using CSM.API.Commands;
using ProtoBuf;

namespace SampleExternalMod.Commands
{
    [ProtoContract]
    public class TestCommand : CommandBase
    {
        [ProtoMember(1)] 
        public string testingData { get; set; }

        [ProtoMember(2)] 
        public string anotherTestingData { get; set; }
    }
}

Sending a command:

public void SomeSpecialFeature()
{
      Command.SendToAll(new TestCommand()
      {
            testingData = "Hello",
            anotherTestingData = "This is a test"
      });
}

Receiving a command:

using CSM.API;
using CSM.API.Commands;

namespace SampleExternalMod.Commands
{
    public class TestHandler : CommandHandler<TestCommand>
    {
        protected override void Handle(TestCommand command)
        {
            Log.Info(command.testingData);
        }
    }
}