Recipes
Short, focused examples for common tasks.
Optional Steam initialization with retry
private SteamNetworkClient client = new SteamNetworkClient();
private bool multiplayerAvailable;
private float nextRetryAt;
public override void OnUpdate()
{
if (!multiplayerAvailable && Time.realtimeSinceStartup >= nextRetryAt)
{
if (client.TryInitialize(out var error))
{
multiplayerAvailable = true;
MelonLogger.Msg("Steam networking ready.");
}
else
{
nextRetryAt = Time.realtimeSinceStartup + 2f;
MelonLogger.Warning($"Steam networking not ready: {error?.Message}");
}
}
if (multiplayerAvailable)
{
client.ProcessIncomingMessages();
}
}
Guard every networking path with multiplayerAvailable. Your local/single-player logic should still run when Steamworks is unavailable.
Find host and remote members
if (client.TryGetHostMember(out var host))
{
MelonLogger.Msg($"Host is {host.DisplayName} ({host.SteamId64})");
}
foreach (var member in client.GetRemoteMembers())
{
MelonLogger.Msg($"Remote member {member.DisplayName}: {member.SteamIdString}");
}
Send a typed transaction message
public class TransactionPayload
{
public string TransactionId { get; set; } = string.Empty;
public string ItemId { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
public class TransactionMessage : TypedP2PMessage<TransactionPayload>
{
public override string MessageType => "MYMOD_TRANSACTION";
public TransactionMessage()
{
}
public TransactionMessage(TransactionPayload payload)
: base(payload)
{
}
}
client.RegisterMessageHandler<TransactionMessage>((message, sender) =>
{
MelonLogger.Msg($"Received {message.Payload.TransactionId} from {sender.m_SteamID}");
});
await client.SendMessageToPlayerAsync(hostId, new TransactionMessage(new TransactionPayload
{
TransactionId = Guid.NewGuid().ToString("N"),
ItemId = "pseudo",
Quantity = 10,
UnitPrice = 25m
}));
Host-authoritative sync var
var roundNumber = client.CreateHostSyncVar("Round", 1);
// Subscribe to changes
roundNumber.OnValueChanged += (oldVal, newVal) =>
{
MelonLogger.Msg($"Round {oldVal} -> {newVal}");
};
// Host sets new value (clients only read)
roundNumber.Value = 2;
Per-client sync var
var isReady = client.CreateClientSyncVar("Ready", false);
// Subscribe to any player's changes
isReady.OnValueChanged += (playerId, oldVal, newVal) =>
{
MelonLogger.Msg($"Player {playerId}: ready={newVal}");
};
// Set my own ready status
isReady.Value = true;
// Check if everyone is ready
var allReady = isReady.GetAllValues().Values.All(r => r);
Broadcast a mod configuration to everyone
var cfg = new DataSyncMessage { Key = "mod_config", Value = JsonConvert.SerializeObject(config) };
await client.BroadcastMessageAsync(cfg);
RPC-like event to a single player
var evt = new EventMessage
{
EventType = "give_item",
Payload = "soil")
};
await client.SendMessageToPlayerAsync(targetId, evt);
Send a screenshot file to the host
var bytes = File.ReadAllBytes("screenshot.png");
await client.SendLargeDataToPlayerAsync(hostId, "screenshot.png", bytes, channel: 1);
For manual chunking, keep each complete serialized packet under the reliable send limit. The payload chunk is smaller than the packet because SteamNetworkLib adds message headers.
var bytes = File.ReadAllBytes("screenshot.png");
int chunk = 64 * 1024;
int total = (int)Math.Ceiling((double)bytes.Length / chunk);
for (int i = 0; i < total; i++)
{
var slice = bytes.Skip(i * chunk).Take(chunk).ToArray();
var msg = new FileTransferMessage
{
FileName = "screenshot.png",
FileSize = bytes.Length,
ChunkIndex = i,
TotalChunks = total,
IsFileData = true,
ChunkData = slice
};
await client.SendMessageToPlayerAsync(hostId, msg, channel: 1);
}
Invite friends via Steam overlay
client.OpenInviteDialog();
Check mod version compatibility
client.SetMyData("mod_version", MyMod.Version);
client.SyncModDataWithAllPlayers("mod_version", MyMod.Version);
if (!client.IsModDataCompatible("mod_version"))
{
MelonLogger.Warning("Players have mismatched mod versions");
}