Modding:Zone Builders

From Caves of Qud Wiki
Jump to navigation Jump to search
This article has information that is missing or not up to par.
Reason: This page is missing several utilities that would be useful for zone building.
This page is about modding. See the modding overview for an abstract on modding.
This page is about modding. See the modding overview for an abstract on modding.
For best results, it's recommended to have read the following topics before this one:

Zone builders are bits of code that allow you to shape the generation of a zone. At the lowest level of detail they are used to place and remove objects from zones; more broadly, they can be used to shape the geometry and encounters that a player runs into in a room.

The game provides a limited suite of generic builders that you may find useful for your mod. In general, if you are making small alterations to a zone you do not need to deal with zone builders. On the other hand, if you're planning to create your own maps with custom procedural generation, you will want to know how to write them by hand.

All code examples on this page assume that you are starting from the starter code from the zones and worlds intro.


Using zone builders

When the player first enters a zone, the game builds it following these steps:

  • First, it identifies the appropriate zone factory for the world that the player is in (JoppaWorld, ThinWorld, etc.).
  • The game then runs CanBuildZone on the zone factory to determine the method that it should use to generate the zone[1].
    • When it returns true, the zone factory calls the factory's BuildZone method to create the zone.
    • Otherwise, the zone factory calls GenerateZone to instantiate the zone and AddBlueprintsFor.
  • In most cases in JoppaWorld, the game takes the AddBlueprintsFor route. In this case, the game looks up the terrain object for the zone and searches Worlds.xml for the zone blueprint that it should apply to the zone[2].
  • The game then iterates over the zone builders defined by the blueprint and applies them successively to the new zone.

Designing a custom zone requires you to select the builders that you want to use for the zone and add them to the zone. In the majority of cases, you will do this statically through Worlds.xml. As an example, here's the specification of MoonStairCell (used to generate generic Moon Stair zones):

<cell Name="MoonStairCell" Inherits="DefaultJoppaCell" ApplyTo="TerrainMoonStair">
  <zone Level="5-9" x="0-2" y="0-2" Name="sky above the Moon Stair" IndefiniteArticle="the" AmbientBed="Sounds/Ambiences/amb_bed_moonstair">
    <builder Class="Sky"></builder>
  </zone>
  <zone Level="10" x="0-2" y="0-2" Name="Moon Stair" IndefiniteArticle="the" AmbientBed="Sounds/Ambiences/amb_bed_moonstair">
    <builder Class="MoonStair"></builder>        
    <builder Class="FactionEncounters" Population="GenericFactionPopulation"></builder>
    <music Track="Reflections of Ptoh" />
    <postbuilder Class="ZoneTemplate:MoonStair"></postbuilder>
  </zone>
  <zone Level="11-15" x="0-2" y="0-2" Name="subterranean stair">                            
    <builder Class="MoonStair"></builder>                                                           
    <builder Class="PossibleCryotube"></builder>
    <builder Class="FactionEncounters" Population="GenericFactionPopulation"></builder>
    <music Track="Reflections of Ptoh" />
    <postbuilder Class="ZoneTemplate:MoonStairCaves"></postbuilder>
  </zone>
</cell>

Cell specifications are broken up by X, Y, and Z-levels. Depending on where the player is, they will trigger different builders appropriate to their location.

Zone builders are also dynamically injected during world generation. For example, this is how Oboroqoru, Ape God's lair, goatfolk yurts, and dynamic villages are placed by JoppaWorldBuilder. It is possible, but less common, to dynamically apply zone builders in other parts of the game.

Generic, pre-existing zone builders

The game provides a limited suite of zone builders that are sufficiently generic that they can be easily applied to any zone.

Builder name Description
AddBlueprintBuilder Adds an object with a specified Object parameter as a blueprint. In general it is preferable to use zone templates and population tables to add (non-structural) objects to zones, so this is primarily useful when you need to dynamically add on a zone builder. For example: this builder is used to place the chest containing the Ruin of House Isner during world generation.
AddWidgetBuilder Adds an object to the (0, 0) cell of a zone. Typically this is a Widget object (see ObjectBlueprints/Widgets.xml) although in theory it can be anything. An AddWidgetBuilder builder is added to the zone whenever you use the <widget/> tag in Worlds.xml. For example,
<widget Blueprint="Grassy" />

adds a Grassy widget (which paints the floor of the zone with grass).

Connecter
FactionEncounters Adds encounters with legendary creatures sampled from a provided table.
MapBuilder This builder loads a map from a .rpm file. A MapBuilder is implicitly to a zone by the <map/> tag in Worlds.xml. For example, the map for the Yd Freehold is added as follows:
<map FileName="YdFreehold.rpm" />
Music Adds a music track to a zone. A Music zone builder is implicitly added by the <music/> tag in Worlds.xml. For example, the music for the salt dunes is added with
<music Track="MoghrayiRemembrance" />
SolidEarth Fills the entire map with shale. This is useful when you wish to carve out the geometry of your zone (i.e., specify the empty space rather than specify the filled space).
StairConnector
StairsDown Adds a set of stairs down to the next Z-level. You can specify X- and Y- coordinates to influence the region in which the stairs are placed.
StairsUp Adds a set of stairs up to the previous Z-level. You can specify X- and Y- coordinates to influence the region in which the stairs are placed.
TileBuilding

In general, the game's existing zone builders tend to be highly monolithic, so any zone builders not listed above are not recommended for use in creating new types of zones. For new zones modders should favor a more modular architecture.

The ZoneBuilderSandbox interface

ZoneBuilderSandbox is the primary interface for creating new zone builders. Any builders that you add to the game will inherit from this class.

Most zone builders only need to define one method: BuildZone. This method accepts a zone instance, and returns a boolean that signals whether or not subsequent zone builders should also run[3]. As an example, here is the definition for the SolidEarth zone builder:

namespace XRL.World.ZoneBuilders;

public class SolidEarth
{
    public bool BuildZone(Zone Z)
    {
        for (int i = 0; i < Z.Width; i++)
        {
            for (int j = 0; j < Z.Height; j++)
            {
                Z.GetCell(i, j).Clear();
                Z.GetCell(i, j).AddObject(GameObjectFactory.Factory.CreateObject("Shale"));
            }
        }
        return true;
    }
}

This builder

  • loops over each cell in the zone (incidentally: nowadays there is a Zone.GetCells method to make this even easier);
  • clears all objects that are currently in the cell; and
  • adds a shale object to the cell.

When designing new zone builders, you should (where possible) prefer to create modular zone builders that can be easily reused. As a general rule, earlier builders should define coarse-grained detail about a zone such as layout and general geometry. Builders that come later should be focused on adding fine-grained details (e.g. individual rooms and encounters) to the zone.

Helpful utilities

In principle, ZoneBuilderSandbox is all that you need in order to build a zone. In practice, you will want to rely on some other utilities when generating zones. This section will cover some of the most important utilities available to you.

EnsureAllVoidsConnected

ZoneBuilderSandbox includes an EnsureAllVoidsConnected that will create paths between empty spaces in your zone. This is particularly useful when you are carving out your zone from another material.

For example: consider the following zone builder:

namespace XRL.World.ZoneBuilders {
    public class CreateVoids : ZoneBuilderSandbox {
        public bool NoisyPath = true;

        public bool BuildZone(Zone Z) {
            foreach (var cell in Z.GetCells()) {
                if (
                    cell.CosmeticDistanceTo(20, 12) < 4 ||
                    cell.CosmeticDistanceTo(50, 12) < 4
                   )
                    cell.Clear();
            }
            EnsureAllVoidsConnected(Z, pathWithNoise: NoisyPath);
            return true;
        }
    }
}

This will create two pockets of empty space -- one centered at coordinates (20, 12), another centered at coordinates (50, 12) -- and connects them with EnsureAllVoidsConnected. By setting pathWithNoise we can make a somewhat randomized path between these voids.

Starting from the code in the introduction, update your Worlds.xml to include the following:

<!-- ... -->
<zone Level="10" x="1" y="1" Name="test zone">
  <builder Class="SolidEarth" />
  <builder Class="CreateVoids" />
  <widget Blueprint="Grassy" />
</zone>
<!-- ... -->

Here is an example of a zone that gets generated with these builders:

EnsureAllVoidsConnected example.webp

Pathfinding FindPath

If you want to create a path directly between two cells, you can use the FindPath interface (which is also used by creature AI to find the shortest path between two points).

Here is an example of the CreateVoids zone builder from the EnsureAllVoidsConnected example using pathfinding (this time with a straight path between the voids).

using XRL.World.AI.Pathfinding;

namespace XRL.World.ZoneBuilders {
    public class CreateVoids : ZoneBuilderSandbox {
        public bool BuildZone(Zone Z) {
            foreach (var cell in Z.GetCells()) {
                if (
                    cell.CosmeticDistanceTo(20, 12) < 4 ||
                    cell.CosmeticDistanceTo(50, 12) < 4
                   )
                    cell.Clear();
            }

            var path = new FindPath(Z.GetCell(20, 12), Z.GetCell(50, 12));
            foreach (var step in path.Steps)
                step.Clear();

            return true;
        }
    }
}

PlacePopulationInRegion

PlacePopulationInRegion samples some objects from a population table and places them in a specified set of cells. It is commonly used to limit the area for an encounter. A more limited version of this is PlacePopulationInRect which works specifically for rectangles.

The following zone builder demonstrates use of PlacePopulationInRegion to create a little circular farm in the center of the zone:

using Genkit;
using System;
using System.Collections.Generic;

namespace XRL.World.ZoneBuilders {
    public class CreateCircularFarm : ZoneBuilderSandbox {
        public bool BuildZone(Zone Z) {
            var region = new List<Location2D>();

            foreach (var cell in Z.GetCells()) {
                var dist = cell.CosmeticDistanceTo(40, 12);
                if (dist > 6)
                    continue;

                cell.Clear();
                if (dist == 6)
                    cell.AddObject("BrinestalkStakes");
                else
                    region.Add(cell.Location);
            }

            PlacePopulationInRegion(Z, region, "PigFarm");

            // Add a little door
            Z.GetCell(30, 12).Clear();
            Z.GetCell(30, 12).AddObject("Brinestalk Gate");

            return true;
        }
    }
}

Create a PopulationTables.xml so that you can define the PigFarm table:

<?xml version="1.0" encoding="utf-8" ?>
<populations>
  <population Name="PigFarm">
    <group Name="Creatures" Style="pickeach">
      <object Blueprint="PigFarmer" />
      <object Blueprint="Herding Dog" />
      <object Blueprint="Farm Pig" Number="3-6" />
    </group>
  </population>
</populations>

And now add the zone builder it to your Worlds.xml with

<zone Level="10" x="1" y="1" Name="test zone">
  <builder Class="CreateCircularFarm" />
  <widget Blueprint="Dirty" />
</zone>

This should produce a tiny farm that looks like the following:

Circular farm.webp

Other utilities

Finally, here are some other less commonly-used utilities that you may nonetheless find helpful for your use case:

Name Class Description
Clear XRL.World.Cell Remove all objects from a cell.
ClearWalls XRL.World.Cell Remove all walls from a cell (preserving any other objects that may be present).
RequireObject XRL.World.Cell Place an object with the specified blueprint in the cell, if one does not already exist.
ClearRect XRL.World.ZoneBuilders.ZoneBuilderSandbox Clears out all of the objects from a rectangle in a given zone.
PlaceHut XRL.World.ZoneBuilders.ZoneBuilderSandbox

References

  1. XRL.World.ZoneManager, method GenerateFactoryZone
  2. XRL.World.ZoneFactories.JoppaWorldZoneFactory, method AddBlueprintsFor
  3. XRL.World.ZoneManager, method ApplyBuilderToZone