Skip to content

How To: Networked RichHud

Aristeas edited this page Aug 17, 2024 · 15 revisions

HEY NERD.

This guide will walk you through the basics of setting up a network synced RichHud display.

Prerequsites

  1. Decent knowledge of Space Engineers modding.
  2. Empty modscript project If you are missing these, please check out Digi's excellent modscripting guide and come back here when you feel ready.

Networking

Note: In this guide, we will only be covering "manual" networking. This is not the ideal situation for a MySync, as there is nothing useful to attach it to.

Setup

Create a Networking folder under your mod's root script folder and add a NetworkController class. In the NetworkController class, add the following scaffold code:

public class NetworkController
{
    public readonly ushort NetworkId;

    public NetworkController(ushort networkId)
    {
        NetworkId = networkId;
        MyAPIGateway.Multiplayer.RegisterSecureMessageHandler(NetworkId, OnReceive);
    }

    /// <summary>
    /// Closes the network controller. This MUST be called, otherwise the mod does not properly reload
    /// </summary>
    public void Close()
    {
        MyAPIGateway.Multiplayer.UnregisterSecureMessageHandler(NetworkId, OnReceive);
    }

    /// <summary>
    /// Sends a packet directly to the server.
    /// </summary>
    /// <param name="packet">The packet to be sent</param>
    public void SendToServer(PacketBase packet)
    {

    }

    /// <summary>
    /// Sends a packet to all clients.
    /// </summary>
    /// <param name="packet"></param>
    public void SendToEveryone(PacketBase packet)
    {

    }

    /// <summary>
    /// Sends a packet to a single client/server.
    /// </summary>
    /// <param name="recipientSteamId"></param>
    /// <param name="packet"></param>
    public void SendToId(ulong recipientSteamId, PacketBase packet)
    {

    }

    private void OnReceive(ushort handlerId, byte[] packet, ulong senderSteamId, bool fromServer)
    {

    }
}

This will be our primary networking class. The PacketBase class will be added next.

Next, create a PacketBase class in the same folder. Put the following scaffold code in it:

/// <summary>
/// Base class for all NetworkController packets.
/// </summary>
[ProtoContract(UseProtoMembersOnly = true)]
public abstract class PacketBase
{
    /// <summary>
    /// Triggered when the packet is received.
    /// </summary>
    public abstract void OnReceive(ulong senderSteamId);
}

This class will act as a standard packet and makes our lives a lot easier. The ProtoContract tag will be explained later.

Public Methods

Switch back to the NetworkController class; we're going to start filling in the methods we created earlier. Keen has a handful of nice networking utilities, but the one we're focusing on today can be found in MyAPIGateway.Multiplayer.SendMessageToServer - SendMessageToServer, SendMessageTo, and SendMessageToOthers. All of these take a ushort Network ID and a byte[] Data (with SendMessageTo also accepting a ulong RecipientSteamId). Having a specific NetworkId is useful because it avoids cross-talk between mods; imagine if you had to deal with the packets from everyone else's mods too! Byte arrays are used for the Data field because it allows for serialization - turning a generic object into bytes. This is necessary to transmit your objects over the internet; your home router has no clue what a SimpleHudPacketis supposed to be.

Go ahead and add each of those three methods to their corresponding methods in NetworkController, and put NetworkId as the first field in each. The controller's NetworkId is specified in the constructor - we'll worry about that later. Yay for generic classes! You may notice that the next fields take a byte[], and there are no byte[]s to be seen. We can turn the PacketBase objects provided in our methods into bytes with the following method:

MyAPIGateway.Utilities.SerializeToBinary(packet);

Put that in as the next field. For the SendToId() method, add recipientSteamId as the last field. You should end up with something like this:

public void SendToServer(PacketBase packet)
{
    MyAPIGateway.Multiplayer.SendMessageToServer(NetworkId, MyAPIGateway.Utilities.SerializeToBinary(packet));
}

public void SendToEveryone(PacketBase packet)
{
    MyAPIGateway.Multiplayer.SendMessageToOthers(NetworkId, MyAPIGateway.Utilities.SerializeToBinary(packet));
}

public void SendToId(ulong recipientSteamId, PacketBase packet)
{
    MyAPIGateway.Multiplayer.SendMessageTo(NetworkId, MyAPIGateway.Utilities.SerializeToBinary(packet), recipientSteamId);
}

Private Methods

Now that we have the public methods out of the way, let's work on the real meat of this class. So far we have a way to send our packets, but nothing happens when they're received! In the constructor, we used the sister method of the ones above:

MyAPIGateway.Multiplayer.RegisterSecureMessageHandler(NetworkId, OnReceive);

This tells Space Engineers to call one of our methods, OnRecieve(), whenever a packet is sent to this client with the specified NetworkId. The OnRecieve() method gets called with a decent number of arguments, some of which we won't be using today - we'll leave them there in case you wish to make a fancier network controller. The important ones in this case are byte[] packet and ulong senderSteamId. The packet argument is still serialized, but good news! With the following method, we can deserialize it (or turn it back into the original object).

MyAPIGateway.Utilities.SerializeFromBinary<PacketBase>(packet);

Note - we have to specify the object type; this is the primary reason we use a base class for all our packets.

Once we have our packet object, we'll trigger its OnReceived() method and our basic NetworkController is done!

MyAPIGateway.Utilities.SerializeFromBinary<PacketBase>(packet)?.OnReceive(senderSteamId);

Note - the null check is important here in case packets are of the wrong type.

A Brief Intro to ProtoBuf

Okay, that was a lot of boilerplate code. Let's get into the more interesting stuff - the data you'll be sending. Create a new class under a new folder in Network, Packets/SimpleHudPacket.cs. It should inheret from PacketBase and implement OnRecieve(); don't worry about the exact implementation yet.

/// <summary>
/// Transfers HUD data.
/// </summary>
[ProtoContract]
public class SimpleHudPacket : PacketBase
{
    public override void OnReceive(ulong senderSteamId)
    {
        
    }
}

Note - The [ProtoContract] tag indicates that the class can be serialized using Protobuf, a common serialization standard.

Next, we need to edit PacketBase - add the following line right above the [ProtoContract] tag in that class:

[ProtoInclude(1, typeof(SimpleHudPacket))]

This tells ProtoBuf to look for SimpleHudPacket as something a PacketBase can be deserialized into. It's easy to miss this, but WOE BE UNTO YOU if you forget. If you want to add more packet types in the future, make sure to give each one a unique ID.

Alright, back to SimpleHudPacket. We've got a really cool setup that holds no data - let's fix that. In this demo we're going to be transmitting a string, but you can use any serializable object (including other classes you make!) Make a new public string object in the class named Text, but don't give it a default value. Add a [ProtoMember] tag with a unique ID in front of it.

[ProtoMember(1)] public string Text;

Note - the [ProtoMember] tag indicates that a field should be serialized. Without it, ProtoBuf ignores the field. WOE BE UNTO YOU if you duplicate an ID.

Next, add your favorite debug output method to the OnReceive() method. Mine is:

public override void OnReceive(ulong senderSteamId)
{
    MyAPIGateway.Utilities.ShowNotification(Text);
}

Congratulations, you're almost done! The next step is to...

Set up MainSession

It's all fine and dandy having a networking class, but it would be pretty cool if we actually used it. Set up a new MainSession.cs in your mod's root script folder as a standard SessionComponent with LoadData and UnloadData. Add another method, void HandleChatMessages(ulong sender, string messageText, ref bool sendToOthers) that gets triggered by MyAPIGateway.Utilities.MessageEnteredSender. Don't forget to remove it too.

[MySessionComponentDescriptor(MyUpdateOrder.NoUpdate)]
public class MainSession : MySessionComponentBase
{
    public static MainSession I;

    public override void LoadData()
    {
        I = this;
        MyAPIGateway.Utilities.MessageEnteredSender += HandleChatMessages;
    }

    protected override void UnloadData()
    {
        MyAPIGateway.Utilities.MessageEnteredSender -= HandleChatMessages;
        I = null;
    }

    private void HandleChatMessages(ulong sender, string messageText, ref bool sendToOthers)
    {
        if (!messageText.StartsWith("/ping"))
            return;
        sendToOthers = false;
        messageText = messageText.Substring(5);
    }
}

With the basic setup out of the way, let's initialize our network. Add a new public field to the MainSession class, NetworkController Network. Don't instantiate it yet - add the constructor to the LoadData() method to make sure everything loads in the correct order. Go ahead and give your network a cool ID too; it doesn't really matter what it is, just make sure you don't use someone else's ID.

public NetworkController Network;

public override void LoadData()
{
    I = this;
    Network = new NetworkController(42069);
    MyAPIGateway.Utilities.MessageEnteredSender += HandleChatMessages;
}

In the HandleChatMessages() method, use your brand new Network object to send a new SimpleHudPacket to everyone. Go ahead and set its Text field to messageText, like so:

private void HandleChatMessages(ulong sender, string messageText, ref bool sendToOthers)
{
    if (!messageText.StartsWith("/ping"))
        return;
    sendToOthers = false;
    messageText = messageText.Substring(5);

    SimpleHudPacket packet = new SimpleHudPacket()
    {
        Text = messageText
    };

    Network.SendToEveryone(packet);
}

Save your mod and try it out in multiplayer - if you send /ping [text] in chat, the other people in the server will see the message on their HUD!

RichHud

Now for the fun part, adding RichHud. You can download the latest version from https://github.com/ZachHembree/RichHudFramework.Client/releases; for now, get the full Source code.zip. Create a new folder under API in your project called RichHud; the files can go anywhere but we'll but them here for simplicity. Extract the .zip file into this new folder, and move everything a layer up from the RichHudFramework.Client-x.x.x.x folder. Your mod should now look like this:

image

Initializing

Initializing RichHud is pretty easy - just add the following line to your LoadData() method:

RichHudClient.Init(ModContext.ModName, CreateHUD, null);

The first argument is your mod name, the second is the OnInit function (called when the API is ready), and the last argument is triggered when the API unloads. The first is required, but the other two can be null. Note - you MUST init the API before using any of its methods!

Add an empty CreateHud() method to MainSession, we'll set up our HUD objects there.

HUD Objects

There's a ton of HUD objects available in RichHud; you can get an overview of the important ones on RichHud.Client's wiki page and get a comprehensive list by looking through the API files. Today we'll use LabelBox but the setup is similar for other objects.

Create a new MyLabelBox field in MainSession, like so:

private LabelBox MyLabelBox ;

Now instantiate it in CreateHud(). Set its parent field to HudMain.HighDpiRoot; there's a ton of others but that's the most universally applicable. Note - you can even make your own hud spaces! Now set its Text field to whatever you want and check it out ingame. Don't forget to add RichHud to your world!

private void CreateHud()
{
    MyLabelBox = new LabelBox(HudMain.HighDpiRoot)
    {
        Text = "I'm a LabelBox!",
    };
}

image

Tie-In with Networking

Now that we have a label and we have a network, let's stick them together. Switch to your SimpleHudPacket and change the MainSession.I.MyLabelBox.Text field to the recieved text, like so:

public override void OnReceive(ulong senderSteamId)
{
    MyAPIGateway.Utilities.ShowNotification(Text);
    MainSession.I.MyLabelBox.Text = senderSteamId + ": " + Text;
}

There we have it! One networked HUD.

What Next?

This was a pretty basic guide, but your mods don't have to be! Here's some ideas to improve on the stuff in here - feel free to reach out to @aristeas. on Discord if you pull any of these off!

  • [easy] Give your LabelBox a fancy background color!
  • [easy] Move the label to the top-left corner of the screen
  • [med] Change the SteamId text output to a player's username
  • [hard] Custom MySync using NetworkController
  • [hard] Message log!