Modding

Hello Mod

Code mod tour: lifecycle logs, console commands hello / hello.ping / hello.beacon / hello.stats, host-player messages, a network prefab, game events and world data.

Updated 11 Oct 2026

Code mod tour: lifecycle logs, console commands hello / hello.ping / hello.beacon / hello.stats, host-player messages, a network prefab, game events and world data.

Mod id example.hello, side both.

A code mod: build it with dotnet build (see Making mods) and put the output folder in the game's Mods folder.

mod.json

{
    "id": "example.hello",
    "name": "Hello Mod",
    "version": "1.2",
    "author": "Seeds & Circuits",
    "side": "both",
    "description": "Code mod tour: lifecycle logs, console commands hello / hello.ping / hello.beacon / hello.stats, host-player messages, a network prefab, game events and world data."
}

HelloMod.cs

using SeedsAndCircuits;
using SeedsAndCircuits.DebugConsole;
using SeedsAndCircuits.Inventory;
using SeedsAndCircuits.Mods;
using UnityEngine;

namespace HelloMod
{
    /// <summary>
    /// The smallest tour of the mod API: lifecycle logs, a console command, messages between host and players, and a
    /// network prefab. Side "both": the host and every player run it (joiners download it from the host).
    /// </summary>
    public sealed class HelloMod : Mod
    {
        static HelloMod s_Instance;

        public override void OnLoad()
        {
            s_Instance = this;
            Log($"Loaded from {Folder}. {ItemCatalog.All.Count} items known so far.");

            // Messages: the same mod on another machine sends these (names are per mod).
            OnMessage("greet", msg => Log($"The host says: {msg.Text}"));
            OnMessage("ping", msg => SendToClients("pong", $"Pong from the host to everyone (client {msg.Sender} pinged)."));
            OnMessage("pong", msg => Log(msg.Text));

            // A network prefab built in code: a glowing post the host can spawn for everyone.
            var beacon = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            beacon.transform.localScale = new Vector3(0.3f, 2f, 0.3f);
            beacon.GetComponent<Renderer>().material.color = new Color(1f, 0.8f, 0.2f);
            Object.Destroy(beacon.GetComponent<Collider>());
            RegisterNetworkPrefab("beacon", beacon);

            // Game events (they fire on the host): count what happens in this world.
            GameEvents.TreeFelled += OnTreeFelled;
            GameEvents.ItemCrafted += OnItemCrafted;
            GameEvents.PlayerDied += OnPlayerDied;
        }

        // ------------------------------------------------------------------ world stats (game events + world data)

        // Saved with the world (WorldData): each world keeps its own counts.
        [System.Serializable]
        sealed class Stats
        {
            public int treesFelled;
            public int itemsCrafted;
            public int deaths;
        }

        Stats _stats = new Stats();

        void OnTreeFelled(string kind, Vector3 position) => _stats.treesFelled++;
        void OnItemCrafted(GameObject player, string station, string itemId, int count) => _stats.itemsCrafted += count;
        void OnPlayerDied(GameObject player, string cause) => _stats.deaths++;

        // Host: written into the world save just before it's saved.
        public override void OnSavingWorld() => WorldData.Set("stats", _stats);

        public override void OnWorldLoaded()
        {
            if (IsServer)
                _stats = WorldData.Get("stats", new Stats());
            Log($"World loaded with {ItemCatalog.All.Count} items. Host: {IsServer}.");
        }

        public override void OnWorldUnloaded() => Log("World closed.");

        public override void OnUnload()
        {
            GameEvents.TreeFelled -= OnTreeFelled;
            GameEvents.ItemCrafted -= OnItemCrafted;
            GameEvents.PlayerDied -= OnPlayerDied;
            Log("Turned off.");
            s_Instance = null;
        }

        // Host only: welcome every player who joins.
        public override void OnPlayerJoined(ulong clientId)
        {
            if (clientId != LocalClientId)
                SendToClient(clientId, "greet", $"Welcome, client {clientId}!");
        }

        public override void OnPlayerLeft(ulong clientId) => Log($"Client {clientId} left.");

        // Any static [ConsoleCommand] in a mod's assembly becomes a console command while the mod is on (written out as
        // ConsoleCommandAttribute here: outside the console's namespace the short name clashes with its ConsoleCommand class).
        [ConsoleCommandAttribute("hello", "Says hello from the Hello Mod example.", Local = true)]
        static string Hello() => "Hello from a mod!";

        // Not Local: runs on the host, which has the stats.
        [ConsoleCommandAttribute("hello.stats", "This world's counts (saved with it): trees felled, items crafted, deaths.")]
        static string StatsCommand()
        {
            if (s_Instance == null || !s_Instance.IsServer)
                return "Only the host keeps the stats.";
            Stats s = s_Instance._stats;
            return $"Trees felled: {s.treesFelled}, items crafted: {s.itemsCrafted}, deaths: {s.deaths}";
        }

        [ConsoleCommandAttribute("hello.ping", "Pings the host; it answers every player.", Local = true)]
        static string Ping()
        {
            if (s_Instance == null || !s_Instance.IsClient)
                return "Not in a game.";
            s_Instance.SendToServer("ping", "ping");
            return "Ping sent.";
        }

        // Not Local: runs on the host (for the host, or for a player the host gave the console with op).
        [ConsoleCommandAttribute("hello.beacon", "Puts a beacon at your feet for everyone (host).")]
        static string Beacon()
        {
            if (s_Instance == null || !s_Instance.IsServer)
                return "Only the host can spawn beacons.";
            Transform player = DebugConsole.FindPlayer();
            if (player == null)
                return "No player.";
            s_Instance.SpawnNetworkPrefab("beacon", player.position + Vector3.up, Quaternion.identity);
            return "Beacon placed.";
        }
    }
}

HelloMod.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <!-- Build settings and game references: ../Mod.props. Output: bin/Hello Mod/ -->
  <PropertyGroup>
    <AssemblyName>HelloMod</AssemblyName>
    <ModFolder>Hello Mod</ModFolder>
  </PropertyGroup>
  <Import Project="../Mod.props" />
</Project>