Skip to content

API Tasks

Macka edited this page Nov 9, 2025 · 1 revision

The Basics

Each subclass of DV.Logic.Job.Task has a unique set of properties, fields and behaviours that require custom serialisation and deserialisation methods.

Multiplayer handles task synchronisation through a TaskNetworkData factory pattern, with custom serialisation routines for each DV.Logic.Job.TaskType. Additionally, the Multiplayer has built-in methods for serialising/deserialising the common fields shared by all task types.

Each serialisation/deserialisation method pair must be registered with the TaskNetworkData factory to enable automatic sync of these tasks.

Serialisation/deserialisation method pairs can be unregistered if your mod is disabled/unloaded, however, Multiplayer's serialisers/deserialisers for base-game TaskTypes can not be unregistered.

Multiplayer has implemented serialisers/deserialisers for all base-game Task, any extensions to these should be done through inheritance and a custom TaskType.

Writing a Task Serialiser/Deserialiser

Writing a TaskNetworkData serialiser/deserialiser requires implementing the abstract class TaskNetworkData<T>.

The methods to be implemented are:

  • T FromTask(Task task)
    • Your implementation must call FromTaskCommon()
  • void Serialize(BinaryWriter writer)
    • Your implementation must call SerializeCommon()
  • void Deserialize(BinaryReader reader)
    • Your implementation must call DeserializeCommon()
  • Task ToTask(ref Dictionary<ushort, Task> netIdToTask)
    • Your implementation must call ToTaskCommon()
    • Your implementation must add the task to the netIdToTask dictionary
  • List<ushort> GetCars()
    • Your implementation must return a non-null List<ushort>

Your implementation needs to collect the unique fields to be serialised when FromTask is called, but does not need to implement code for gathering and serialising the common fields. Similarly, ToTask() needs to recreate the task and populate the unique fields, but does not need to deserialise or populate the common fields.

Common fields serialised/deserialised for you:

  • ushort taskNetId (this is the NetId assigned to the task by Multiplayer)
  • TaskState state
  • float taskStartTime
  • float taskFinishTime
  • bool IsLastTask
  • float TimeLimit

Caution

It is critical that the serialisation and deserialisation is balanced and completed in the same order, otherwise data corruption and de-syncs are likely to occur.

WarehouseTask Example

This section has been provided as an example of an inbuilt serialiser/deserialiser for a base-game TaskType.

A WarehouseTask contains the following unique fields:

  • List<DV.Logic.Job.Car> cars
  • DV.Logic.Job.WarehouseTaskType warehouseTaskType
  • DV.Logic.Job.WarehouseMachine warehouseMachine
  • DV.ThingTypes.CargoType cargoType
  • float cargoAmount
  • bool readyForMachine

Extracting the fields / FromTask()

public class WarehouseTaskData : TaskNetworkData<WarehouseTaskData>
{
    // Fields to be serialised
    public ushort[] CarNetIDs { get; set; }
    public WarehouseTaskType WarehouseTaskType { get; set; }
    public string WarehouseMachine { get; set; }
    public CargoType CargoType { get; set; }
    public float CargoAmount { get; set; }
    public bool ReadyForMachine { get; set; }

    public override WarehouseTaskData FromTask(Task task)
    {
        if (task is not WarehouseTask warehouseTask)
            throw new ArgumentException("Task is not a WarehouseTask");

        // Mandatory call to FromTaskCommon()
        FromTaskCommon(task);

        // Extract list of cars and get their netIds
        CarNetIDs = warehouseTask.cars.Select
        (
            car =>
            {
                if (car == null || !MultiplayerAPI.Instance.TryGetNetId(car, out var netId))
                    return (ushort)0;

                return netId;
            }
        ).ToArray();

        // Extract standard WarehouseTask fields
        WarehouseTaskType = warehouseTask.warehouseTaskType;
        WarehouseMachine = warehouseTask.warehouseMachine.ID;
        CargoType = warehouseTask.cargoType;
        CargoAmount = warehouseTask.cargoAmount;
        ReadyForMachine = warehouseTask.readyForMachine;

        return this;
    }
}

Serialising the data / Serialize()

public class WarehouseTaskData : TaskNetworkData<WarehouseTaskData>
{
    public ushort[] CarNetIDs { get; set; }
    public WarehouseTaskType WarehouseTaskType { get; set; }
    public string WarehouseMachine { get; set; }
    public CargoType CargoType { get; set; }
    public float CargoAmount { get; set; }
    public bool ReadyForMachine { get; set; }
    
    // Prior methods ...

    public override void Serialize(BinaryWriter writer)
    {
        // Mandatory call to SerializeCommon()
        SerializeCommon(writer);

        // Write each field (note: WriteUShortArray() is a utility provided by Multiplayer API)
        writer.WriteUShortArray(CarNetIDs);
        writer.Write((byte)WarehouseTaskType);
        writer.Write(WarehouseMachine ?? string.Empty);
        writer.Write((int)CargoType);
        writer.Write(CargoAmount);
        writer.Write(ReadyForMachine);
    }
}

Deserialising the data / Deserialize()

public class WarehouseTaskData : TaskNetworkData<WarehouseTaskData>
{
    public ushort[] CarNetIDs { get; set; }
    public WarehouseTaskType WarehouseTaskType { get; set; }
    public string WarehouseMachine { get; set; }
    public CargoType CargoType { get; set; }
    public float CargoAmount { get; set; }
    public bool ReadyForMachine { get; set; }

    // Prior methods ...

    public override void Deserialize(BinaryReader reader)
    {
        // Mandatory call to DeserializeCommon()
        DeserializeCommon(reader);

        // Read each field (note: ReadUShortArray() is a utility provided by Multiplayer API)
        CarNetIDs = reader.ReadUShortArray();
        WarehouseTaskType = (WarehouseTaskType)reader.ReadByte();
        WarehouseMachine = reader.ReadString();
        CargoType = (CargoType)reader.ReadInt32();
        CargoAmount = reader.ReadSingle();
        ReadyForMachine = reader.ReadBoolean();
    }
}

Converting deserialised data into a task / ToTask()

public class WarehouseTaskData : TaskNetworkData<WarehouseTaskData>
{
    public ushort[] CarNetIDs { get; set; }
    public WarehouseTaskType WarehouseTaskType { get; set; }
    public string WarehouseMachine { get; set; }
    public CargoType CargoType { get; set; }
    public float CargoAmount { get; set; }
    public bool ReadyForMachine { get; set; }

    // Prior methods ...

    public override Task ToTask(ref Dictionary<ushort, Task> netIdToTask)
    {
        // Retrieve Car objects from netIds
        List<Car> cars = CarNetIDs.Select(netId => MultiplayerAPI.Instance.TryGetObjectFromNetId(netId, out Car car) ? car : null)
            .OfType<Car>()
            .ToList();

        // Create the Task using the constructor
        WarehouseTask newWarehouseTask = new
        (
           cars,
           WarehouseTaskType,
           JobSaveManager.Instance.GetWarehouseMachineWithId(WarehouseMachine),
           CargoType,
           CargoAmount,
           (long)TimeLimit,
           IsLastTask
        );

        // Mandatory call to ToTaskCommon()
        ToTaskCommon(newWarehouseTask);

        // Populate fields not set in the constructor
        newWarehouseTask.readyForMachine = ReadyForMachine;

        // Add the Task and its netId to the dictionary
        netIdToTask.Add(TaskNetId, newWarehouseTask);

        return newWarehouseTask;
    }
}

Return list of Car NetIds / GetCars()

public class WarehouseTaskData : TaskNetworkData<WarehouseTaskData>
{
    public ushort[] CarNetIDs { get; set; }
    public WarehouseTaskType WarehouseTaskType { get; set; }
    public string WarehouseMachine { get; set; }
    public CargoType CargoType { get; set; }
    public float CargoAmount { get; set; }
    public bool ReadyForMachine { get; set; }

    // Prior methods ...

    public override List<ushort> GetCars()
    {
        return CarNetIDs.ToList();
    }
}

SequentialTasks Example

This section has been provided as an example of an inbuilt serialiser/deserialiser for a base-game TaskType.

A SequentialTasks contains the following unique fields:

  • LinkedList<DV.Logic.Job.Task> tasks
  • LinkedListNode<DV.Logic.Job.Task> currentTask

Extracting the fields / FromTask()

public class SequentialTasksData : TaskNetworkData<SequentialTasksData>
{
    public TaskNetworkData[] Tasks { get; set; }

    public override SequentialTasksData FromTask(Task task)
    {
        if (task is not SequentialTasks sequentialTasks)
            throw new ArgumentException("Task is not a SequentialTasks");

        // Mandatory call to FromTaskCommon()
        FromTaskCommon(task);

        // Serialise all sub-tasks
        Tasks = MultiplayerAPI.Instance.ConvertTasks(sequentialTasks.tasks);

        return this;
    }
}

Serialising the data / Serialize()

public class SequentialTasksData : TaskNetworkData<SequentialTasksData>
{
    public TaskNetworkData[] Tasks { get; set; }

    // Prior methods ...

    public override void Serialize(BinaryWriter writer)
    {
        // Mandatory call to SerializeCommon()
        SerializeCommon(writer);

        // Write the length of the Tasks array
        writer.Write((byte)Tasks.Length);

        // Call Serialize() on each sub-Task
        foreach (var task in Tasks)
        {
            writer.Write((byte)task.TaskType);
            task.Serialize(writer);
        }
    }
 }

Deserialising the data / Deserialize()

public class SequentialTasksData : TaskNetworkData<SequentialTasksData>
{
    public TaskNetworkData[] Tasks { get; set; }

    // Prior methods ...

    public override void Deserialize(BinaryReader reader)
    {
        // Mandatory call to DeserializeCommon()
        DeserializeCommon(reader);

        // Read the length of the list of Tasks
        var tasksLength = reader.ReadByte();

        // Size the Tasks array and read each serialised Task
        Tasks = new TaskNetworkData[tasksLength];
        for (int i = 0; i < tasksLength; i++)
        {
            // Read the TaskType
            var taskType = (TaskType)reader.ReadByte();

            // Request an empty TaskNetworkData for the TaskType
            Tasks[i] = MultiplayerAPI.Instance.ConvertTask(taskType);
            Tasks[i].Deserialize(reader);
        }
    }

}

Converting deserialised data into a task / ToTask()

public class SequentialTasksData : TaskNetworkData<SequentialTasksData>
{
    public TaskNetworkData[] Tasks { get; set; }

    // Prior methods ...

    public override Task ToTask(ref Dictionary<ushort, Task> netIdToTask)
    {
        List<Task> tasks = [];

        // Convert each TaskNetworkData in the sub-Tasks
        foreach (var task in Tasks)
        {
            var taskResults = task.ToTask(ref netIdToTask);
            tasks.Add(taskResults);
        }

        // Create the Task using the constructor
        SequentialTasks newSequentialTask = new(tasks, (long)TimeLimit);

        // Mandatory call to ToTaskCommon()
        ToTaskCommon(newSequentialTask);

        // Add the Task and its netId to the dictionary
        netIdToTask.Add(TaskNetId, newSequentialTask);

        // Rebuild linked list task states - this is the equivalent of SequentialTasks.OverrideTaskState()
        int index = 0;
        for (var currentNode = newSequentialTask.tasks.First; currentNode != null; currentNode = currentNode.Next)
        {
            currentNode.Value.state = tasks[index].state;
            currentNode.Value.taskStartTime = tasks[index].taskStartTime;
            currentNode.Value.taskFinishTime = tasks[index].taskFinishTime;

            if (tasks[index].state == TaskState.Done && currentNode != newSequentialTask.tasks.Last)
                newSequentialTask.currentTask = currentNode.Next;

            index++;
        }

        return newSequentialTask;
    }
}

Return list of Car NetIds / GetCars()

public class SequentialTasksData : TaskNetworkData<SequentialTasksData>
{
    public TaskNetworkData[] Tasks { get; set; }

    // Prior methods ...

    public override List<ushort> GetCars()
    {
        List<ushort> result = [];

        foreach (var task in Tasks)
        {
            var cars = task.GetCars();
            result.AddRange(cars);
        }

        return result;
    }
}

Registering a Task Serialiser/Deserialiser

Once you have written your serialiser/deserialiser you must register it. Registration can be done any time after the Multiplayer API has initialised, but should be done prior to any job sync occurring. The ideal time to complete the registration is when you are registering for Server and Client Started/Stopped events.

Registration requires calling RegisterTaskType<TCustomTask, TTaskNetworkData>(), using your custom Task as the first type argument, your TaskNetworkData class as the second type argument, and passing in the unique DV.Logic.Job.TaskType (you will need to extend this enum).

Example:

public static class MyMod
{
    public static UnityModManager.ModEntry ModEntry;

    public static bool Load(UnityModManager.ModEntry modEntry)
    {
        ModEntry = modEntry;
        
        // Your mod initialisation here
        // ...
        if (MultiplayerAPI.IsMultiplayerLoaded)
        {
            MultiplayerAPI.Instance.SetModCompatibility(ModEntry.Info.Id, MultiplayerCompatibility.All);

            // Register custom task types for Multiplayer serialisation and deserialisation.
            MultiplayerAPI.Instance.RegisterTaskType<MyCustomTask, MyCustomTaskData>(MyCustomTask.TaskType);
            
            MultiplayerAPI.ServerStarted += OnServerStarted;
            MultiplayerAPI.ClientStarted += OnClientStarted;
        }

        return true;
    }
}

Unregistering a Task Serialiser/Deserialiser

If you need to unregister a task serialiser/deserialiser, simply call UnregisterTaskType<TCustomTask>() passing in the unique DV.Logic.Job.TaskType.

Important

Prior to unregistering a task serialiser/deserialiser, ensure there are no more Jobs containing this task type in existance, otherwise de-syncs may occur. The safest time to call UnregisterTaskType() is when a game session is not in progress.

Installing
Hosting
Joining a Game
Mod Compatibility

API Overview
Getting Started
Guides
API Reference

Clone this wiki locally