Modding

Volcano Island

A new world type (New World > World): an island with a volcano, a crater lake, ash fields and lava-tube caves. Viggo sells sulfur.

Updated 11 Oct 2026

A new world type (New World > World): an island with a volcano, a crater lake, ash fields and lava-tube caves. Viggo sells sulfur.

Mod id example.volcanoisland, 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.volcanoisland",
    "name": "Volcano Island",
    "version": "1.0",
    "author": "Seeds & Circuits",
    "side": "both",
    "description": "A new world type (New World > World): an island with a volcano, a crater lake, ash fields and lava-tube caves. Viggo sells sulfur."
}

VolcanoIsland.cs

using SeedsAndCircuits.Mods;
using SeedsAndCircuits.Terrain;
using Unity.Mathematics;
using UnityEngine;

namespace VolcanoIsland
{
    /// <summary>
    /// A world type made by a mod: New World ▸ World ▸ "Volcano Island". The game generates its island as usual, then the
    /// mod raises a volcano in the middle with a crater lake, covers its slopes in ash (a biome of its own), digs more
    /// caves and lets Viggo sell sulfur. Every machine runs the same code on the same seed, so host and players get the
    /// same volcano (side "both": joining players download the mod).
    /// </summary>
    public sealed class VolcanoIsland : Mod
    {
        const string WorldTypeId = "example.volcano";

        TerrainBiomeDefinition _ash;
        TerrainMaterialDefinition _ashGround;
        Texture2D _ashTexture;

        public override void OnLoad()
        {
            AddWorldType(WorldTypeId, "Volcano Island");
            // Undone last (registrations are undone in reverse), after the game let go of the biome and material.
            WhenTurnedOff(() =>
            {
                Object.Destroy(_ash);
                Object.Destroy(_ashGround);
                Object.Destroy(_ashTexture);
            });

            // A terrain material of its own: dark volcanic ash, soft to dig, giving sand. Its texture is made here (a mod
            // with a bundle would load a TerrainMaterialDefinition asset with real textures); any size works.
            _ashTexture = AshTexture(256);
            _ashGround = ScriptableObject.CreateInstance<TerrainMaterialDefinition>();
            _ashGround.name = "Volcanic Ash";
            _ashGround.displayName = "Volcanic Ash";
            _ashGround.topAlbedo = _ashTexture;
            _ashGround.debugColor = new Color(0.22f, 0.21f, 0.2f);
            _ashGround.tilingMeters = 3f;
            _ashGround.smoothness = 0.05f;
            _ashGround.variation = 0.2f;
            _ashGround.hardness = 0.6f;
            _ashGround.dropItemId = "sand";
            AddTerrainMaterial(_ashGround);

            // A biome made in code (or a TerrainBiomeDefinition asset from a bundle): ash ground, no trees or grass.
            _ash = ScriptableObject.CreateInstance<TerrainBiomeDefinition>();
            _ash.name = "Ash Fields";
            _ash.displayName = "Ash Fields";
            _ash.color = new Color(0.35f, 0.33f, 0.32f);
            _ash.grassDensity = 0f;
            _ash.groundMaterial = _ashGround;
            AddBiome(_ash);

            // Content works the same way: everything added here is removed again when the mod is turned off.
            AddTrade("viggo", "sulfur", price: 6, stock: 20);
        }


        /// <summary>Dark grey grit with lighter specks.</summary>
        static Texture2D AshTexture(int size)
        {
            var texture = new Texture2D(size, size, TextureFormat.RGBA32, true) { name = "Volcanic Ash", wrapMode = TextureWrapMode.Repeat };
            var pixels = new Color32[size * size];
            var random = new System.Random(7);
            for (int y = 0; y < size; y++)
            for (int x = 0; x < size; x++)
            {
                // Tileable noise: the sum of waves that repeat across the texture.
                float n = 0.5f + 0.25f * Mathf.Sin(2f * Mathf.PI * (3 * x + 5 * y) / size) * Mathf.Cos(2f * Mathf.PI * (4 * y - 2 * x) / size);
                float grain = (float)random.NextDouble();
                byte v = (byte)Mathf.Clamp(38 + n * 30 + grain * 22 + (grain > 0.97f ? 70 : 0), 0, 255);
                pixels[y * size + x] = new Color32(v, (byte)(v * 0.96f), (byte)(v * 0.92f), 255);
            }
            texture.SetPixels32(pixels);
            texture.Apply(true, false);
            return texture;
        }

        static bool IsVolcano(TerrainWorldGeneration world) => world.type == WorldTypeId;

        /// <summary>
        /// The start island (generated worlds also have outer islands, raised one by one as they're made; their maps
        /// start at <see cref="ProceduralWorldParams.Offset"/>). The volcano only goes on the start island.
        /// </summary>
        static bool IsStartIsland(ProceduralMap map) => math.all(map.Params.Offset == 0f);

        /// <summary>Lower hills elsewhere, so the volcano stands out.</summary>
        public override void OnConfigureWorld(TerrainWorldGeneration world, ref ProceduralWorldParams landscape)
        {
            if (!IsVolcano(world))
                return;
            landscape.MountainHeight *= 0.4f;
            landscape.LandShare = math.max(landscape.LandShare, 0.55f);
        }

        /// <summary>The cone and its crater, straight into the height map (640 x 640 cells over the map and a sea margin).</summary>
        public override void OnMapGenerated(TerrainWorldGeneration world, ProceduralMap map)
        {
            if (!IsVolcano(world) || !IsStartIsland(map))
                return;
            float2 center = Center(world);
            float radius = world.size * 0.16f;          // foot of the cone
            float peak = math.min(130f, world.size * 0.06f);
            float craterRadius = radius * 0.16f;
            float craterFloor = peak - 22f;
            int res = map.Res;
            for (int z = 0; z < res; z++)
            for (int x = 0; x < res; x++)
            {
                int i = x + z * res;
                float2 p = map.Origin + new float2(x, z) * map.Cell;
                float d = math.distance(p, center);
                if (d >= radius)
                    continue;
                // A concave cone (steep near the top) with a rim, the crater dug into it.
                float t = 1f - d / radius;
                float cone = peak * t * t * (3f - 2f * t) * (0.6f + 0.4f * t);
                float h = math.max(map.Heights[i], cone + map.Heights[i] * (1f - t));
                if (d < craterRadius)
                    h = math.lerp(craterFloor, h, math.smoothstep(craterRadius * 0.6f, craterRadius, d));
                map.Heights[i] = h;
                if (map.Kinds[i] == ProceduralMap.Sea && h > 1f)
                    map.Kinds[i] = ProceduralMap.Land;
            }

            // The crater lake: a lake the game fills with water (ProceduralMap.Lakes, cells marked as lake).
            float water = craterFloor + 6f;
            int cells = 0;
            for (int z = 0; z < res; z++)
            for (int x = 0; x < res; x++)
            {
                int i = x + z * res;
                float2 p = map.Origin + new float2(x, z) * map.Cell;
                if (math.distance(p, center) < craterRadius && map.Heights[i] < water)
                {
                    map.Kinds[i] = ProceduralMap.LakeCell;
                    cells++;
                }
            }
            map.Lakes.Add(new ProceduralLake
            {
                Center = center, Radius = craterRadius, WaterLevel = water, Depth = water - craterFloor, Cells = cells,
            });
        }

        /// <summary>Ash on the volcano's slopes, ragged at the edge.</summary>
        public override void OnBiomesGenerated(TerrainWorldGeneration world, TerrainBiomePainter biomes)
        {
            if (!IsVolcano(world) || !IsStartIsland(biomes.Map))
                return;
            float2 center = Center(world);
            float radius = world.size * 0.16f * 0.75f;
            for (int z = 0; z < biomes.Cells; z++)
            for (int x = 0; x < biomes.Cells; x++)
            {
                float2 p = biomes.CellCenter(x, z);
                float edge = radius * (1f + 0.15f * noise.snoise(p / 70f + world.seed % 1000));
                if (math.distance(p, center) < edge)
                    biomes.SetCell(x, z, _ash);
            }
        }

        /// <summary>More and bigger caves under a volcano (lava tubes).</summary>
        public override void OnConfigureGround(TerrainWorldGeneration world, ref TestGeneratorParams ground)
        {
            if (IsVolcano(world))
                ground.CaveDensity *= 1.5f;
        }

        /// <summary>The middle of the map (the island's middle).</summary>
        static float2 Center(TerrainWorldGeneration world) => new float2(world.size * 0.5f, world.size * 0.5f);
    }
}

VolcanoIsland.csproj

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