Closed Pathoschild closed 6 years ago
A few modders implemented their own networking code, documented here for reference.
PyTK provides net APIs for other mods to use.
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.
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).
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();
});
TehPers.Core.Multiplayer is an upcoming framework that provides net APIs for other mods to use.
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.
A few mods use the game's underlying APIs directly.
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();
}
Here are the tentative APIs in the initial implementation. More features beyond these (like synchronised fields) may be added in future versions.
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;
}
};
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);
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.
Done in develop
for the upcoming SMAPI 2.8, and documented on the wiki.
Add an API to let mods perform common multiplayer tasks, like getting a multiplayer ID or broadcasting a message.
To do
SMAPI 2.6
GetNewID
andGetActiveLocations
).SMAPI 2.8-beta.6
SMultiplayer
class.SMAPI 2.8-beta.7