Skip to content

(Feat) Multiplayer

David Karnok edited this page Jul 17, 2023 · 82 revisions

This page documents the (Feat) Multiplayer mod.

Introduction

This mod adds (some level of) multiplayer support to The Planet Crafter.

Currently, this means the ability to play a world (new or existing save) by two or more players (host, clients).

Installation

  • Install BepInEx.

  • 🔽 Download the akarnokd-FeatMultiplayer.zip for both the host and client.

  • Extract the zip into the BepInEx\plugins directory.

    • Please keep the folder inside the zip so you'll end up with BepInEx\plugins\akarnokd - (Feat) Multiplayer
  • Ask the host for its IP address.

    • If running on the same network (both computers behind NAT or a router), have the host start the game, then on the right side, read off the IP address (without the port) on the main screen.
  • On the client, open akarnokd.theplanetcraftermods.featmultiplayer.cfg, find [Client], and set HostAddress to the host's IP address. Example:

    [Client]
    
    ## The IP address where the Host can be located from the client.
    # Setting type: String
    # Default value: 
    HostAddress = 192.168.0.100
    

⚠️ Warnings

⚠️ The mod is under active development. Things may not work, things may get weird, things may get out of sync.

⚠️ Currently the mod doesn't support encrypted connections between the host and the client.

⚠️ Play with someone you trust. Not everything is validated yet and most client messages are accepted at face value.

⚠️ Backup your save!

⚠️ Patches the vanilla game all over the place so this mod and other mods may break each other.

Troubleshooting

The game and this mod creates log files in the game's save directory:

%USERPROFILE%\AppData\LocalLow\MijuGames\Planet Crafter

  • Player.log (both sides)
  • Player_Client_[name].log (client side only)
  • Player_Host.log (host side only)

When reporting issues, please post these logs and if possible, post the save of the Host Survival-x.json as well.

Configuration

The configuration file is in BepInEx\config\akarnokd.theplanetcraftermods.featmultiplayer.cfg

Keyboard shortcuts

The following shortcuts work only when the player is not in an UI window. Some keys also require holding the Accessibility Key (default CTRL, vanilla settings):

Shortcut Description
G Show Emote wheel.
H Toggle showing the location info of other players in an overlay.
CTRL+I Spawn chests with all sorts of items on the host.
CTRL+P Trigger all kinds of meteor events on the host (one after the other).
CTRL+H Toggle health/food/water drain (both sides).

Features

👍 These should work

  • Host a co-op world by using one of the (existing or new) saves.
  • Join a co-op world (up to 25 players, limited to 8 by default).
  • The host world remembers the backpack and equipment of the client.
  • See each other on the screen as Astronaut Avatar.
  • See each other move around.
  • Emote to each other.
  • Change coloring of the host and client avatars via config.
  • Sync up basic Terraformation status; i.e., grass, water shows for both players.
  • Place buildings and items, such as machines, containers, furniture, etc.
  • Place doors/windows on buildings
  • Crafting
  • Mine resources
  • Grab items dropped to the ground
  • Add and remove items to/from player-built containers
  • Items dropped animate on the client side
  • Doors open for the other player
  • Unlock blueprint microchips; both players receive unlocks
  • Set text on containers, labels
  • Set colors on beacons
  • Interacting with pre-placed chests (blue/golden)
  • Terraformation speed
  • UPnP port mapping
  • Launching rockets by both parties
  • See meteor shower and impacts on both sides
  • Accessing machine inventories
  • Asteroid resources syncing
  • Grabbing vegetables or algae
  • Seeing the same trees grow
  • Sync of day-night cycle
  • Sync environmental effects (storms, rains, etc)
  • Equipping and unequipping equipment (client)
  • Deconstructing on the client
  • Game mode sync (chill, standard, intense, etc)
  • Dying and death chests (standard only)
  • Story elements (messages)
  • See each other's flashlights
  • Terrain greening upon joining the host
  • Shredding items
  • Recycling items
  • DNA Manipulator
  • Picking up larvae

⚠️ These may not work properly

  • Story events (should trigger meteor events, but never got that far in testing).
  • Spawning of special larvae
  • The new machines of 0.5.005 (I can't even name them, sorry).

⛔ These don't work properly or at all (but I know about them)

  • No jetpack effects
  • Asteroid rocks not in sync or not visible
  • Sky backdrop sync (moons can look out of phase)

👀 See also: known issues.

Mod compatibility

Depending on what other mods do to the game, some of them may or may not work properly, at all, or even break this mod. See the sections below for working, non-working and unknown mods.

ℹ️ All sorts of mods can be found here:

Compatible mods

Legend:

  • 🌵 works, but some action needs to be taken
  • 🆗 works, can be on either or both host and client
  • 👻 ghost only
Mod name Author Version Link Status
(Cheat) Asteroid Landing Position Override 😊 latest File 🆗 1
(Cheat) Auto Consume 😊 latest File 🆗
(Cheat) Auto Launch Rockets 😊 latest File 🆗 1
(Cheat) Auto Sequence DNA 😊 latest File 🆗 1
(Cheat) Inventory Stacking 😊 latest File 🌵 2
(Cheat) Highlight Nearby Resources 😊 latest File 🆗
(Cheat) Teleport To Nearest Minable 😊 latest File 🌵 1
(Fix) International Loading 😊 latest File 🆗
(Feat) Command Console 😊 latest File 🌵 3
(Feat) Space Cows 😊 latest File 🌵 2
(Misc) Plugin Update Checker 😊 latest File 🆗
(Save) Automatic Save 😊 latest File 🌵 1
(UI) Continue 😊 latest File 🆗
(UI) Custom Inventory Sort All 😊 latest File 🌵 2
(UI) Hungarian Translation 😊 latest File 🆗
(UI) Italian Translation 😊 latest File 🆗
(UI) Menu Shortcut Keys 😊 latest File 🆗
(UI) Overview Panel 😊 latest File 🆗
(UI) Show Consumable Counts 😊 latest File 🆗
(UI) Show MultiTool Mode 😊 latest File 🆗
(UI) Show Player Inventory Counts 😊 latest File 🆗
(UI) Show Rocket Counts 😊 latest File 🆗
(UI) Sort Saves 😊 latest File 🆗
Asteroid Tweaks Lathrey 2.1.0 Lathrey-AsteroidTweaks.dll 🌵 2
Auto Move Lathrey 2.5.0 Lathrey-AutoMove.dll 🆗
Disable Build Constraints Lathrey 2.5.0 Lathrey-DisableBuildConstraints.dll 🆗
Improve Performance Lathrey 2.5.0 Lathrey-ImprovePerformance.dll 🆗
Toggle Fog Lathrey 2.5.0 Lathrey-ToggleFog.dll 🆗
Better Jetpack aedenthorn 0.1.1 Nexus 🆗
Custom Flashlight aedenthorn 0.2.0 Nexus 🌵 2
Drill Sound aedenthorn 0.1.0 Nexus 🆗
Remote Storage Access aedenthron 0.2.1 Nexus 🆗
Quick Rotate aedenthorn 0.1.0 Nexus 🆗
Quick Store aedenthron 0.3.6 Nexus 🆗
Show Next Unlockables aedenthron 0.1.0 Nexus 🆗
Fix Shallow Water CarlKenner 0.0.1 Link 🆗
Fix Units CarlKenner 0.0.1 Link 🆗
Discord Presence Ludeo 1.0.0 Discord.Presence.zip 🆗
Terraformation Details Overlay Ludeo 1.0.0 Terraformation.Details.Overlay.zip 🆗
Craft The Planet! doublestop 0.0.2 CraftThePlanet-0.0.2.zip 🆗
Compass Always Visible doublestop 0.0.1 CompassAlwaysVisible-0.0.1.zip 🆗
Custom Progress Speed MikePdog 1.0.1.1 mikeypdog-CustomProgressSpeed.zip 🌵 2
No Fall Damage cisox 1.0.0.0 Nexus 🆗
Toggle Beacons cisox 1.0.0.0 Nexus 🆗
  • 1 Detects the multiplayer mod and disables itself on the client in multiplayer games.
  • 2 Install on both the host and the client. Ensure their configs match on all sides.
  • 3 Spawning items doesn't last on the client because the host will delete such objects.

Incompatible mods

Legend:

  • 🌵 doesn't work on its own but there might be a workaround
  • ⛔ doesn't work at all and can't be fixed from this end
Mod name Author Version Link Status
OreExtractorTweaks Lathrey <= 2.1.0 Lathrey-OreExtractorTweaks.dll 🌵 1
Day Night Cycle Tweaks Lathrey <= 2.1.0 Lathrey-DayNightCycleTweaks.dll 2
Creative Mode skrwoor <= 0.0.0.3 Nexus 🌵 3
Mobile Crafter skrwoor <= 0.0.0.1 Nexus 4
Auto Mine aedenthorn <= 0.2.2 Nexus 5
Better Meteorites aedenthorn <= 0.1.1 Nexus 6
Craft from Containers aedenthorn <= 0.3.2 Nexus 7
Spawn Objects aedenthron <= 0.3.0 Nexus 8
Storage Customization aedenthorn <= 0.3.2 Nexus 9
Shortcuts doublestop 0.0.5 Shortcuts-0.0.5.zip 🌵 10
Always Deconstruct cisox 1.0.0.3 Nexus 11
Keep items on death cisox 1.0.0.0 Nexus 11
Set respawn point cisox 1.0.0.0 Nexus 11
  • 1 May work when the (Cheat) Machine Remote Deposit mod of mine is also installed.
  • 2 Runs with its own state and bypasses the vanilla day-night cycle.
  • 3 Dying on the client may not work properly, free crafting might not work.
  • 4 Conflicting overrides (TryToCraftInWorld, AddItemInEquipment, RemoveItemFromEquipment).
  • 5 Creates objects on the client (CheckForNearbyMinables).
  • 6 Very likely conflicting overrides (Asteroid_CreateImpact_Patch).
  • 7 Conflicting overrides; the host has to deduce materials.
  • 8 Creates objects on the client (WorldObjectsHandler.CreateXXX use).
  • 9 Modifies inventory size on the client, patches equipment change callbacks (not used on client).
  • 10 Quickload borks the gamestate and network state on both host or client.
  • 11 Conflicting overrides.

Unknown

Anything not listed in the previous two sections not yet tested with this mod.

Mod name Author Version Link Status
* 😊 latest File ⚠️ not tried them yet

Future plans

I haven't decided about these yet.

  • Ingame chat.
  • Expose message sending and receiving via API to mods.

FAQ

Can more than one client join?

Yes.

Will the mod support more than one client?

It does as of version 0.2.0.0

Why does the other player look like that?

We don't have a full player model and I couldn't get anything other than these flat-images to work.

UPnP says error

There could be a problem reaching your nearby router or NAT device, it doesn't support UPnP, or you are not behind NAT/router. If your router does support UPnP, you may need to manually add port mapping in the device's setup.

API

The mod specifies a public API that can be used for

  • detecting the current mode (singleplayer or multiplayer),
  • creating and processing custom messages,
  • sending standard objects as messages,
  • retrieving client backpack and equipment inventories.

They are defined as delegate fields on the FeatMultiplayer.Plugin class to avoid paying the cost of reflective method calls for softly-depending mods.

Also the delegates use standard types only so that other mods don't have to depend on the binary/dll of the multiplayer mod for custom types.

Accessing the API

First of all, a third party mod should define a (soft) dependency on this mod via the BepInDepencency annotation:

Via a helper class

FeatMultiplayerApi.cs

I recommend taking the class by source and using it in your project so it doesn't need a hard dependency on the multiplayer mod's dll.

FeatMultiplayerApi mApi = FeatMultiplayerApi.Create();

if (mApi.IsAvailable()) {
    Logger.LogInfo("Multiplayer mod found!");

    Logger.LogInfo("Current state: " + mApi.GetState());
}

Manually

using BepInEx;

[BepInDependency(modFeatMultiplayerGuid, BepInDependency.DependencyFlags.SoftDependency)]
class SomePlugin : BaseUnityPlugin {

    const string modFeatMultiplayerGuid 
         = "akarnokd.theplanetcraftermods.featmultiplayer";

}

(I recommend using a const string for this as we'll need the mod's Guid next.)

Next, in preferably the Awake method, locate the the multiplayer mod's plugin object, and read off the fields starting with api.

using BepInEx.Bootstrap;

private void Awake() 
{

    if (Chainloader.PluginInfos.TryGetValue(
            modFeatMultiplayerGuid, out BepInEx.PluginInfo pi)) 
    {

        Func<string> apiGetMultiplayerMode = GetApi(pi, "apiGetMultiplayerMode");

        Func<string, int> apiCountByGroupId = GetApi(pi, "apiCountByGroupId");

        Action<Func<int, string, ConcurrentQueue<object>, bool>> apiAddMessageParser
              = GetApi(pi, "apiAddMessageParser");

        Action<Func<object, bool>> apiAddMessageReceiver
              = GetApi(pi, "apiAddMessageReceiver");

        Action<int, object> apiSend = GetApi(pi, "apiSend");

        Action<int> apiSignal = GetApi(pi, "apiSignal");

        Action<int, WorldObject> apiSendWorldObject = GetApi(pi, "apiSendWorldObject");

        Action<object, Action<object>> apiSuppressInventoryChangeWhile
              = GetApi(pi, "apiSuppressInventoryChangeWhile");

        Func<int, Inventory> apiGetClientBackpack
              = GetApi(pi, "apiGetClientBackpack");

        Func<int, Inventory> apiGetClientEquipment
              = GetApi(pi, "apiGetClientEquipment");
    }   

    // For convenience
    static T GetApi<T>(BepInEx.PluginInfo pi, string name) 
    {
        return (T)AccessTools.Field(pi.Instance.GetType(), name).GetValue(null);
    }
}

Of course, put the values into a (static) field inside your plugin.

API details

apiGetMultiplayerMode

Returns the current mode in string format: MainMenu, SinglePlayer, CoopHost or CoopClient.

Usage suggestion: call it in patched methods to check if multiplayer-specific behavior is necessary.

switch (apiGetMultiplayerMode()) 
{
    case "CoopHost": 
    {
        // Custom host behavior
        break;
    }
    case "CoopClient":
    {
        // Custom client behavior
        break;
    }
}

apiCountByGroupId

Returns the count of existing WorldObject under a specific group identifier string.

This is more effective than counting them via WorldObjectsHandler.GetAllWorldObjects() as the multiplayer mod already tracks these counts.

int biomassRockets = apiCountByGroupId("RocketBiomass1");

apiAddMessageParser

Registers a Func object that will be called when a non-standard message is parsed for a client on the background thread.

This function will receive the client identifier, the raw message string and a queue to talk to the UI thread. The function should return true if it successfully identified and parsed the message so no other parsers need to be tried.

🚫 Multi-client is currently not supported so the clientId parameter is ignored.

// register the method
apiAddMessageParser(ParseMyMessage);

// the custom object parsed
class MyMessage {
    int clientId;
    string content;
}

// the parser
static bool ParseMyMessage(int clientId, string str, ConcurrentQueue<object> queue) {
    if (str.StartsWith("MyMessage=")) 
    {
        queue.Enqueue(new MyMessage() 
        {
            clientId = clientId,
            content = str.Substring(10)
        });
        return true;
    }
    return false;
}

apiAddMessageReceiver

Registers a Func that will receive the non-standard parsed message object and perform the necessary actions on the UI thread.

This function will receive the parsed message object and should return true if it successfully processed the message.

// register the method
apiAddMessageReceiver(ReceiveMyMessage);

// the receiver
static bool ReceiveMyMessage(object obj)
{
    if (obj is MyMessage myMessage) {
        MijuTools.Managers.GetManager<BaseHudHandler>()
            .DisplayCursorText("", 3f, myMessage.content);
        return true;
    }
    return false;
}

apiSend

Queues up an object to be sent to a particular or all clients.

The object should implement ToString in a way that can be identified by a parser on the other side.

⚠️ The only requirement is that the string returned by ToString does not accidentally contain the newline \n character as it is used to identify the boundaries of messages.

ℹ️ In CoopClient mode, the clientId parameter should be zero, indicating the target is the host. In CoopHost mode, a clientId of zero means broadcast to all clients; non-zero targets a specific client.

🚫 Multi-client is currently not supported so the clientId parameter is ignored.

ℹ️ The string conversion will happen on the background thread.

ℹ️ The message is only queued and may not be sent immediately. Use the apiSignal to send it out over the network immediately.

ℹ️ To send a WorldObject, use apiSendWorldObject.

int clientId = 0; // all clients or to the host.
apiSend(clientId, new MyMessageToSend() { content = "Hello world!" });

class MyMessageToSend {
    string content;
    public override string ToString()
    {
        return "MyMessage=" + content + "\n";
    }
}

You can also create the string upfront and send it directly:

apiSend(0, "MyMessage=Hello World!\n");

apiSignal

Signals the network stack to send out any queued-up messages immediately.

Usually this should be called right after an apiSend call.

🚫 Multi-client is currently not supported so the clientId parameter is ignored.

apiSend(0, "MyMessage=Hello World!\n");

apiSignal(0);

apiSendWorldObject

Sends a WorldObject out immediately to the target client.

🚫 Multi-client is currently not supported so the clientId parameter is ignored.

ℹ️ there is no need to call apiSignal after this method.

using SpaceCraft;

Group group = GroupsHandler.GetGroupViaId("Iron");
WorldObject wo = WorldObjectsHandler.CreateNewWorldObject(group, 0);

apiSendWorldObject(wo);

apiSuppressInventoryChangeWhile

Allows performing an action and not send out the related inventory change messages.

It takes an object and an action, which will receive the former object when executing.

Inventory inventory = InventoriesHandler.GetInventoryById(1);
apiSuppressInventoryChangeWhile(inventory, AddToInventory);

static void AddToInventory(object inv) 
{
    var inventory = inv as Inventory;

    Group group = GroupsHandler.GetGroupViaId("Iron");
    WorldObject wo = WorldObjectsHandler.CreateNewWorldObject(group, 0);
    apiSendWorldObject(wo);

    inventory.AddItem(wo);
}

apiGetClientBackpack

On the host, it returns the given client's shadow backpack inventory, or null if not present.

Use it to put items into the client's inventory.

🚫 Multi-client is currently not supported so the clientId parameter is ignored.

Inventory inv = apiGetClientBackpack(1);
if (inv != null) 
{
    apiSuppressInventoryChangeWhile(inventory, AddToInventory);
}

apiGetClientEquipment

On the host, it returns the given client's shadow equipment inventory, or null if not present.

Use it to put equipment into the client's equipment slots.

🚫 Multi-client is currently not supported so the clientId parameter is ignored.

Inventory inv = apiGetClientEquipment(1);
if (inv != null) 
{
    Group group = GroupsHandler.GetGroupViaId("Backpack1");
    WorldObject wo = WorldObjectsHandler.CreateNewWorldObject(group, 0);
    apiSendWorldObject(wo);

    inv.AddItem(wo);
}

apiSendInventory

Host only. Sends the entire inventory object to the client, including its size and content.

ℹ️ The inventory gets automatically created on the client if it didn't exist.

🚫 Multi-client is currently not supported so the clientId parameter is ignored.

ℹ️ The message is only queued and may not be sent immediately. Use the apiSignal to send it out over the network immediately.

Inventory inv = apiGetClientEquipment(1);

inv.GetInsideWorldObjects().Clear();
inv.SetSize(6);

apiSendInventory(1, inv);
apiSignal(1);
Clone this wiki locally