-
-
Notifications
You must be signed in to change notification settings - Fork 124
Nebula mod API
Add a reference to the nuget.org package DysonSphereProgram.Modding.NebulaMultiplayerModApi to your project.
If you use the new SDK format for your csproj you can simply add
<PackageReference Include="DysonSphereProgram.Modding.NebulaMultiplayerModApi" Version="*.*" IncludeAssets="compile" />
to a PropertyGroup within your project's .csproj file.
- Don't add NebulaAPI dll to your Thunderstore archive.
- Add
Nebula Compatible
tag to inform user that this mod is compatible with multiplayer mod.
Although not recommended, but if you're using NebulaAPI as a hard dependency:
- Add
"nebula-NebulaMultiplayerModApi-2.0.0",
line to your dependencies inmanifest.json
- Mention in your README in the manual installation guide that the NebulaAPI plugin is required.
If you want to make NebulaAPI as a soft dependency, you need to encapsulate all calls and reference to NebulaAPI inside a warper class, and only access it if NebulaAPI.dll is present. For example:
[BepInPlugin(GUID, NAME, VERSION)]
// When a plugin use NebulaAPI as dependency, Nebula will make sure both host and client have the same plugin version
[BepInDependency("dsp.nebula-multiplayer-api", BepInDependency.DependencyFlags.SoftDependency)]
public class YourMod : BaseUnityPlugin
{
public const string GUID = "your.plugin.guid";
public const string NAME = "YourMod";
public const string VERSION = "1.0.0";
void Awake()
{
//...
// Only call when the multiplayer main mod is present
if (BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("dsp.nebula-multiplayer"))
NebulaWarper.OnAwake();
}
}
public class NebulaWarper
{
public static bool IsMultiplayerActive; // Let main plugin use this for flow control
public static bool IsClient;
public static void OnAwake()
{
// The register and hooking have to be done before the multiplayer game starts
NebulaModAPI.RegisterPackets(Assembly.GetExecutingAssembly());
NebulaModAPI.OnMultiplayerGameStarted += OnMultiplayerGameStarted;
NebulaModAPI.OnMultiplayerGameEnded += OnMultiplayerGameEnded;
}
public static void OnMultiplayerGameStarted()
{
IsMultiplayerActive = NebulaModAPI.IsMultiplayerActive;
IsClient = IsMultiplayerActive && NebulaModAPI.MultiplayerSession.LocalPlayer.IsClient;
}
public static void OnMultiplayerGameEnded()
{
IsMultiplayerActive = false;
IsClient = false;
}
}
You can also make warper as an extra plugin to let Chainloader decide. If you need to sync mod settings on joining the server, implement IMultiplayerModWithSettings
instead of IMultiplayerMod
. You only need to do this if your mod changes behavior drastically depending on the config.
[BepInPlugin(GUID, NAME, VERSION)]
[BepInDependency(NebulaModAPI.API_GUID)]
public class YourModNebulaCompat : BaseUnityPlugin, IMultiplayerModWithSettings
{
public const string GUID = "your.nc.plugin.guid";
public const string NAME = "YourMod Nebula Compat";
public const string VERSION = "1.0.0";
public string Version => YourMod.VERSION;
public bool CheckVersion(string hostVersion, string clientVersion)
{
// You can do more complex version checking here
return hostVersion.Equals(clientVersion);
}
void Awake()
{
//...
NebulaModAPI.RegisterPackets(Assembly.GetExecutingAssembly());
}
public void Export(BinaryWriter w)
{
// Export data here
}
public void Import(BinaryReader r)
{
// Import data here
}
}
You can access some classes from Nebula by using NebulaModAPI class getters.
The following are some frequently used ones.
// To know if the player is currently in a multiplayer game
NebulaModAPI.IsMultiplayerActive
// To know if current session is server or client
NebulaModAPI.MultiplayerSession.IsServer
NebulaModAPI.MultiplayerSession.IsClient
// All the information relative to the current local player
NebulaModAPI.MultiplayerSession.LocalPlayer
NebulaModAPI.MultiplayerSession.LocalPlayer.Id //playerId
NebulaModAPI.MultiplayerSession.LocalPlayer.Data.Username //playerUsername
// To send a packet to the server / clients
NebulaModAPI.MultiplayerSession.Network.SendPacket()
// Broadcast the packet to players that are in the same star system
NebulaModAPI.MultiplayerSession.Network.SendPacketToLocalStar()
There are some event hooks too. For more look at the source code of NeulaModAPI.cs.
Common: OnMultiplayerGameStarted
, OnMultiplayerGameEnded
Client: OnStarLoadRequest
, OnDysonSphereLoadFinished
, OnPlanetLoadRequest
, OnPlanetLoadFinished
Server: OnPlayerJoinedGame
, OnPlayerLeftGame
This provides some methods to send packets.
Common: SendPacket
, SendPacketToLocalStar
, SendPacketToLocalPlanet
Server: SendToMatching
, SendPacketToStar
, SendPacketToPlanet
, SendPacketExclude
, SendPacketToStarExclude
This records some player mecha data that is only accessible on the server.
Create a class and follow this example:
public class YourCustomPacket
{
// Only use auto properties here
public int planetId { get; set; }
// Save any primitive type here
public int customValue { get; set; }
// If you have data structures that are not primitives use Binary Reader and Writer provided by NebulaModAPI class and save data into bytearray
public byte[] data { get; set; }
public YourCustomPacket() { } //Make sure to keep default constructor
// If you need more examples check NebulaModel/Packets subfolder
public YourCustomPacket(int planetId, int customValue)
{
this.planetId = planetId;
this.customValue = customValue;
// Using Binary Writer
using IWriterProvider p = NebulaModAPI.GetBinaryWriter();
// Write to p.BinaryWriter here
data = p.CloseAndGetBytes();
}
}
[RegisterPacketProcessor] // This attribute lets Nebula know that this is the processor class for your new packet type
public class YourCustomPacketProcessor : BasePacketProcessor<YourCustomPacket>
{
public override void ProcessPacket(YourCustomPacket packet, INebulaConnection conn)
{
PlanetData planet = GameMain.galaxy.PlanetById(packet.planetId);
// Handle received packets here. If you need more examples check NebulaNetwork/PacketProcessors subfolder
// Using Binary Reader
using IReaderProvider p = NebulaModAPI.GetBinaryReader(packet.data);
// Read from p.BinaryReader here
}
}
When sending packets you need to know on which side (client or server) your code is running. To do that you can use LocalPlayer class in your code. Note that packet processors already have IsHost
and IsClient
properties.
ILocalPlayer localPlayer = NebulaModAPI.MultiplayerSession.LocalPlayer;
INetworkProvider network = NebulaModAPI.MultiplayerSession.Network;
if (localPlayer.IsClient)
{
// Send packet to host (If running on client)
network.SendPacket(new YourCustomPacket(planetId, yourData));
}
else
{
// Send packet to all players within one star system
network.SendPacketToStar(new YourCustomPacket(planetId, yourData), GameMain.galaxy.PlanetById(planetId).star.id);
}
If your mod adds custom data that exists for each planet or star you can listen to events in NebulaModAPI and send packets accordingly:
// This is request packet, clients will send it when they need to load a planet
public class CustomFactoryLoadRequest
{
public int planetId { get; set; }
public CustomFactoryLoadRequest() { }
public CustomFactoryLoadRequest(int planetId)
{
this.planetId = planetId;
}
}
[RegisterPacketProcessor]
public class CustomFactoryLoadRequestProcessor : BasePacketProcessor<CustomFactoryLoadRequest>
{
public override void ProcessPacket(CustomFactoryLoadRequest packet, INebulaConnection conn)
{
if (IsClient) return;
PlanetData planet = GameMain.galaxy.PlanetById(packet.planetID);
PlanetFactory factory = GameMain.data.GetOrCreateFactory(planet);
using IWriterProvider p = NebulaModAPI.GetBinaryWriter();
// Write to p.BinaryWriter here
conn.SendPacket(new CustomFactoryData(packet.planetID, p.CloseAndGetBytes()));
}
}
// This is a response packet from server with requested data
public class CustomFactoryData
{
public int planetId { get; set; }
public byte[] binaryData { get; set; }
public CustomFactoryData() { }
public CustomFactoryData(int id, byte[] data)
{
planetId = id;
binaryData = data;
}
}
[RegisterPacketProcessor]
public class CustomFactorDataProcessor : BasePacketProcessor<CustomFactoryData>
{
internal static Dictionary<int, byte[]> pendingData = new Dictionary<int, byte[]>();
public override void ProcessPacket(CustomFactoryData packet, INebulaConnection conn)
{
if (IsHost) return;
// We need to wait until the factory is fully load on client
// Note that for star packets you can process the packet here
pendingData.Add(packet.planetId, packet.binaryData);
}
// This is actual place where we process the packet
public static void ProcessBytesLater(int planetId)
{
if (!NebulaModAPI.IsMultiplayerActive || NebulaModAPI.MultiplayerSession.LocalPlayer.IsHost) return;
if (!pendingData.TryGetValue(planetId, out byte[] bytes)) return;
pendingData.Remove(planetId);
using IReaderProvider p = NebulaModAPI.GetBinaryReader(bytes);
PlanetFactory factory = GameMain.galaxy.PlanetById(planetId).factory;
// Read from p.BinaryReader here
}
}
// In your Plugin Awake call
NebulaModAPI.OnPlanetLoadRequest += planetId =>
{
NebulaModAPI.MultiplayerSession.Network.SendPacket(new CustomFactoryLoadRequest(planetId));
};
NebulaModAPI.OnPlanetLoadFinished += CustomFactorDataProcessor.ProcessBytesLater;