SMAPI - Stardew Modding API

SMAPI - Stardew Modding API

971k Downloads

Add multiplayer API

Pathoschild opened this issue · 4 comments

commented

Add an API to let mods perform common multiplayer tasks, like getting a multiplayer ID or broadcasting a message.

To do

SMAPI 2.6

  • Add API with basic methods (GetNewID and GetActiveLocations).

SMAPI 2.8-beta.6

  • Plan full multiplayer API.
  • Sync SMAPI context between connected players (game/SMAPI version + installed mods).
  • Disable versions of MTN which remove SMAPI's SMultiplayer class.
  • Add send/receive message API.
  • Add multiplayer events for incoming/outgoing connections, etc.

SMAPI 2.8-beta.7

  • Fix context sync not happening for players connected via Steam friends.
commented

A few modders implemented their own networking code, documented here for reference.

PyTK

PyTK provides net APIs for other mods to use.

APIs

  • Send/receive a string:

    // send
    PyNet.sendMessage("SomeModChannel", "some string");
    
    // receive
    string str = PyNet
       .getNewMessages("SomeModChannel")
       .LastOrDefault()
       .message;
  • Send/receive a data model (note that both ends don't necessarily need a PyMessenger, it can send/receive messages from the other APIs):

    // init (on both ends)
    PyMessenger<MyClass> messenger = new PyMessenger<MyClass>("SomeModChannel");
    
    // send
    MyClass envelope = new MyClass(params);
    messenger.send(envelope, SerializationType.JSON);
    
    // receive
    MyClass[] messages = messenger.receive().ToArray();
  • Send a data model and optionally get a response from the destination mod:

    // init (on both ends)
    PyResponder<bool, MyClass> responder = new PyResponder<bool, MyClass>(
       "SomeModChannel",
       (MyClass data) =>
       {
          /* handle incoming sync */;
          return true;
       }
    );
    responder.start();
    
    // send sync value
    PyNet.sendRequestToAllFarmers<bool>(
       address: "SomeModChannel",
       request: new MyClass(...),
       callback: (bool response) =>
       {
          /* handle response from destination mod */;
       },
       serializationType: SerializationType.JSON
    );
  • Send game assets. This supports Map, Dictionary<string, string>, Dictionary<int, string>, and Texture2D. The message will be received by PyTK on the other end, which will deserialize the content and inject it into the local game's content managers by patching the asset returned by SMAPI. For example:

    // send map to another player
    Map map = ...;
    PyNet.syncMap(map, "MapName", Game1.player);

    Note: since the incoming asset is patched in once, it'll be lost if the asset gets invalidated or it's loaded through a different content manager.

Notes

  • Pros:
    • Easy to use.
    • Supports custom data models (using JSON under the hood).
    • Supports syncing assets (see caveat above).
    • Handles farmhand→farmhand messages automatically. PyTK will route the original message to the host player, and their PyTK instance will resend it to the destination farmhand. (Requires PyTK on the host player.)
    • Resistant to version changes (e.g. added/removed message fields won't cause errors).
  • Cons:
    • Multiple mods can't consume the same message.
    • No trace logging for issues (e.g. messages to an invalid player ID are silently discarded).

References

SpaceCore

SpaceCore provides low-level net APIs for other mods to use. These are undocumented and presumably intended for spacechase0's mods (though Equivalent Exchange uses them too).

APIs

  • Send/receive bytes:
    // send
    using (var stream = new MemoryStream())
    using (var writer = new BinaryWriter(stream))
    {
       writer.Write("some string");
       writer.Write(42);
       Networking.BroadcastMessage("SomeModChannel", stream.ToArray());
    }
    
    // receive
    Networking.RegisterMessageHandler("SomeModChannel", IncomingMessage message => 
    {
       string str = message.Reader.ReadString();
       int num = message.Reader.ReadInt32();
    });

Notes

  • Pros:
    • Simple implementation.
  • Cons:
    • Low-level APIs (mods need to binary serialise/deserialise themselves).
    • Doesn't allow farmhand→farmhand messages.
    • Vulnerable to version changes (e.g. added/removed message fields will likely cause errors).

References

TehPers.Core.Multiplayer

TehPers.Core.Multiplayer is an upcoming framework that provides net APIs for other mods to use.

APIs

  • Send/receive binary-serialisable fields:
    // init (on both ends)
    IMultiplayerApi net = this.GetCoreApi().GetMultiplayerApi();
    
    // send
    net.SendMessage("SomeModChannel", writer =>
    {
       writer.Write("some string");
       writer.Write(42);
    });
    
    // receive
    net.RegisterMessageHandler("SomeModChannel", (sender, reader) =>
    {
       string str = message.Reader.ReadString();
       int num = message.Reader.ReadInt32();
    });
  • Automatically synchronise a value (similar to net fields):
    // create synchronised field
    ISynchronizedWrapper<int> field = 0.MakeSynchronized();
    netApi.Synchronize("unique field name", field);
    
    // read synchronised field
    netApi.GetSynchronized<int>("unique field name");
    
    // read/update value
    int value = field.Value;
    field.Value = 42;
    Note: values are synchronised every eighth update tick.

Notes

  • Pros:
    • Easy to use.
    • Supports synchronised fields.
  • Cons:
    • Multiple mods can't consume the same message.
    • Doesn't allow farmhand→farmhand messages.
    • Vulnerable to version changes (e.g. added/removed message fields will likely cause errors).

Ad-hoc

A few mods use the game's underlying APIs directly.

APIs

  • Send binary-serialisable fields:
    // send
    byte messageTypeID = 50;
    OutgoingMessage message = new OutgoingMessage(messageTypeID, Game1.player, new object[] { "some string", 42 });
    if (Game1.IsClient)
       Game1.client.sendMessage(message);
    else if (Game1.IsServer)
       Game1.server.sendMessage(peerId, message);
    
    // receive (called via custom Multiplayer.processIncomingMessage patch or override)
    void ReceiveMapPing(IncomingMessage message)
    {
       string str = message.Reader.ReadString();
       int num = message.Reader.ReadInt32();
    }

Notes

  • Pros:
    • Nothing needed besides the game itself.
  • Cons:
    • Low-level APIs.
    • Doesn't allow farmhand→farmhand messages.
    • Vulnerable to version changes (e.g. added/removed message fields will likely cause errors).
    • Vulnerable to messageID collisions (we can only have 255 unique message IDs, including game + SMAPI messages).
    • Some mods use Harmony patching, which is likely to cause conflicts when multiple mods do it.
commented

Proposed implementation

Here are the tentative APIs in the initial implementation. More features beyond these (like synchronised fields) may be added in future versions.

Send/receive messages

Mods will send data using helper.Multiplayer.SendMessage. The destination can range from very narrow (e.g. one mod on one player computer) to very broad (all mods on all computers).

Method signature:

/// <summary>Send a message to mods installed by connected players.</summary>
/// <typeparam name="T">The data type. This can be a class with a default constructor, or a value type.</typeparam>
/// <param name="message">The data to send over the network.</param>
/// <param name="channel">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
/// <param name="modIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
/// <param name="playerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
/// <exception cref="ArgumentNullException">The <paramref="message" /> or <paramref="messageType" /> is null.</exception>
void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null);

For example, this will send the current player's position to the same mod on all other computers:

ExampleData data = new ExampleData(Game1.player.getTileLocation());
helper.Multiplayer.SendMessage(data, "SetPlayerPosition", modIDs: new[] { this.Manifest.UniqueID });

Mods will receive data by hooking into the multiplayer events:

helper.Events.Multiplayer.MessageReceived += this.OnMessageReceived;

private void OnMessageReceived(object sender, MessageReceivedEventArgs e)
{
    if (e.FromModID == this.Manifest.UniqueID && e.MessageType == "SetPlayerPosition")
    {
        ExampleData data = e.ReadAs<ExampleData>();
        Vector2 position = data.Position;
    }
};

Fetch remote installed mods

When a player joins a game, it will broadcast a message containing their basic modding metadata (game/SMAPI versions, OS, mod IDs/versions, etc). Thanks to the game already using ReliableOrdered message mode, SMAPI can guarantee that the info is available before the player finishes connecting. This information will be available to mods using this method:

/// <summary>Get the modding metadata for a connected player.</summary>
/// <param name="playerID">The <see cref="Farmer.UniqueMultiplayerID" /> value for the player whose metadata to fetch.</param>
/// <exception cref="InvalidOperationException">There's no player connected with the given ID.</exception>
IModContext GetRemoteMetadata(long playerID);
commented

Note that syncing mod lists has some privacy implications. For example, someone might have installed a potentially embarrassing mod (e.g. a nudity mod) before playing with friends/family, not realising they may see the mod is installed.

To address that, I suggest adding a message to the co-op screen to the effect of 'other players may see what mods you have', and potentially have a UI to let them uncheck mods in-game they want hidden. That way most mod integrations work, but if they have sensitive mods they can keep those hidden.

commented

Done in develop for the upcoming SMAPI 2.8, and documented on the wiki.