Modding:Worlds
This page is about modding. See the modding overview for an abstract on modding. |
Worlds.xml
defines many of the important zones on the world map. This includes both static mapped content (such as the Joppa village layout) as well as dynamic zone builders (such as the builders that are used to generate underground caves).
The game's primary "world", called JoppaWorld, contains most of the game content, but it is theoretically possible to add additional worlds.
In addition to the XML, the game code includes a few hooks that a programmer can use to modify the world state during the world generation process. These hooks are particularly useful if you're planning to add dynamically generated content similar to villages, lairs, sultan historical sites, etc, because they give you access to the same data that the core game uses to create that kind of content.
XML Structure
Worlds.xml
has the following general structure:
XML Tag | Description |
---|---|
<worlds>
|
|
<world>
|
auto merged with the game definition if the world with this Name was already defined (the primary game world currently is JoppaWorld, so you want to use that if you're trying to merge into Qud's world) |
<builder>
|
Any builder class you specify gets added to the list of builders and executed as part of world creation. Must be a class in the XRL.World.WorldBuilders namespace. The only builder used by the base game is JoppaWorldBuilder. Begin a Class name with minus (-) to remove all builders with that class from existing world definition (this would probably be a bad idea though unless you're remaking the entire world by fully replacing JoppaWorldBuilder). |
<cell>
|
Cell nodes are completely overwritten if they have a matching Name to one that already exists in the game files. Cell nodes can inherit from other cell nodes. |
<zone>
|
Define the specified zone or range of zones within this world cell. For example, the Rustwell is a single world cell, which includes various zone definitions for the zones included within it's 3x3 parasang area and Z depths. The game currently applies a depth cap of 49 to zone definitions (in XRL.World.ZoneManager.GetZoneBlueprint ). As a result, if you define a zone definitions with a Level attribute higher than 49, those definitions are ignored.
|
<builder>
|
Builders for zones. Zones can have multiple builders, which are applied in succession to generate the zone. |
<postbuilder>
|
Postbuilders are similar to builders, but applied afterward. |
<population>
|
Use the given population table to generate creatures and items in the zone. |
<map>
|
Use the provided map (an .rpm file) for the zone.
|
World Generation Code
The game exposes two interfaces that a modder may hook into to apply custom logic before or after the world generation process.
JoppaWorldBuilderExtension
This interface is called before and after JoppaWorld generation.
using XRL.World.WorldBuilders;
namespace YourMod.YourNamespace
{
//The game code instantiates an instance of this class during the JoppaWorld generation process
[JoppaWorldBuilderExtension]
public class YourJoppaWorldBuilderExtension : IJoppaWorldBuilderExtension
{
public override void OnBeforeBuild(JoppaWorldBuilder builder)
{
//The game calls this method before JoppaWorld generation takes place. JoppaWorld generation includes the creation of lairs, historic ruins, villages, and more.
}
public override void OnAfterBuild(JoppaWorldBuilder builder)
{
//The game calls this method after JoppaWorld generation takes place.
}
}
}
WorldBuilderExtension
This interface is called before and after the overall world generation process runs (it is not specific to JoppaWorld).
The format is pretty much identical to JoppaWorldBuilderExtension above, except that the attribute is [WorldBuilderExtension]
and the interface is IWorldBuilderExtension
.
Worldgen design patterns
Dynamic secret generation at worldgen
A common use case for IJoppaWorldBuilderExtension
/ IWorldBuilderExtension
is to dynamically add secrets to random locations on the map. In the base game this task is performed by JoppaWorldBuilder
, and covers things like
- placing Bey Lah in a random flower fields tile;
- placing the snapjaw who wields Stopsvalinn to a random desert canyons tile;
- placing the lairs of the Girsh Nephilim and adding zonebuilders for each of those layers;
and so on.
In general, the steps for adding your own secret to Qud's map are as follows:
- Create your own world builder extension class inheriting from (most likely)
IJoppaWorldBuilderExtension
. - In the
OnAfterBuild
method, grab a random mutable block of terrain using eitherAddMutableEncounterToTerrain
orpopMutableLocationOfTerrain
. - Add zone builders (see Modding:Zone Builders) to the zone that will add any creatures, objects, and structures to the zone that you see fit.
- Use
AddSecret
to add a corresponding secret to the location. - (Optional) If you want to make the secret findable while traversing the world map, get the
TerrainTravel
part on the block of terrain that you popped and add a newEncounterEntry
to it.
The code example below demonstrates how we would go through these steps to add a secret creature to a random hills tile.
using XRL;
using XRL.World;
using XRL.World.WorldBuilders;
using XRL.World.ZoneBuilders;
namespace YourMod.YourNamespace
{
[JoppaWorldBuilderExtension]
public class YourJoppaWorldBuilderExtension : IJoppaWorldBuilderExtension
{
public override void OnAfterBuild(JoppaWorldBuilder builder)
{
var location = builder.popMutableLocationOfTerrain("Hills", centerOnly: false);
var zoneID = builder.ZoneIDFromXY("JoppaWorld", location.X, location.Y);
// Change these parameters as appropriate for the secret that you're
// adding.
// - The second parameter affects how the secret appears in the journal
// - The third parameter affects which factions will sell the secret.
// - The fourth parameter affects the category under which the secret
// shows up in the journal.
// - The fifth parameter is the ID for the secret, which can be revealed
// using e.g. the revealsecret wish.
var secret = builder.AddSecret(
zoneID,
"the location of Secret Creature",
new string[2] { "lair", "robot" },
"Lairs",
"$myname_mymod_mysecret"
);
// Add zone builders to the zone.
//
// Each zone builder has a different priority (the integer parameter that
// is passed in). Builders with lower priority run before builders with
// higher priority.
var zoneManager = The.ZoneManager;
// Add some roads to the north and south
zoneManager.AddZoneBuilder(zoneID, ZoneBuilderPriority.LATE, nameof(RoadNorthMouth));
zoneManager.AddZoneBuilder(zoneID, ZoneBuilderPriority.LATE, nameof(RoadSouthMouth));
// Add an object to the zone
// Replace "Oboroqoru" with the object ID of the creature you want to add
var creature = GameObject.Create("Oboroqoru");
zoneManager.AddZonePostBuilder(zoneID, nameof(AddObjectBuilder), "Object", zoneManager.CacheObject(creature));
// You can also set various properties on the zone, if you wish.
zoneManager.SetZoneName(zoneID, "lair of My Creature", Article: "the", Proper: true);
zoneManager.SetZoneIncludeStratumInZoneDisplay(zoneID, false);
zoneManager.SetZoneProperty(zoneID, "NoBiomes", "Yes");
}
}
}
Refer to the source of JoppaWorldBuilder
for more examples.
Creating your own world
For various reasons, you may want to create your own world during the course of writing a mod. In general, it is recommended that you use Interior zones (used by Golems and mechas, for instance), when possible, in place of building your own world. Interior zones can be used to simulate a new world in many cases using XML alone. However, interiors have various limitations that may make them inappropriate for your use case.
Some situations in which you may want to construct your own world include
- You wish to create a secluded area similar to Tzimtzlum that should be accessible from many different points (if you only need a single access point, you may wish to consider using an interior zone instead).
- You want to create a world with a custom world map that players can explore.
- You want your world to have some kind of weird geometric or navigational properties; for example, your world exists on an elliptic plane and loops back around after you've gone a sufficient distance.
- You want to construct a world that avoids some of the constraints of existing worlds (e.g. the Z-level limitations that exist in JoppaWorld).
Components of a world
At a bare minimum, you need the following two things to create a world:
- A world blueprint, constructed by specifying the world in
Worlds.xml
. - A zone factory for the world, i.e. an implementation of
IZoneFactory
.
The world blueprint specifies high-level properties of the world. The zone factory is in charge of constructing zones when you visit your custom world.
In addition to these two components, you may wish to have one or more custom WorldBuilder
s that run on game generation for your world.
Creating a world blueprint
All you need to start constructing your world is to specify a bare-bones <world>...</world>
:
<?xml version="1.0" encoding="utf-8" ?>
<worlds>
<world Name="MyWorld" ZoneFactory="MyWorldFactory" DisplayName="your personal world">
<!--
You can add custom world builders here, e.g.
<builder Class="MyWorldBuilder" />
-->
</world>
</worlds>
This tells the game to define a new WorldBlueprint
under the name MyWorld
, using the MyWorldFactory
zone factory (which we will define shortly).
In some cases you may wish to specify a Plane
or Protocol
for your world. Semantically, these attributes are interpreted as follows:
- Worlds may share the same
Plane
if they are in the same "dimension"; for example, a world consisting of a map of the salt desert andJoppaWorld
. - The
Protocol
encodes properties about a specific world which may or may not be shared across the same plane.
For example, Tzimtzlum
has its own plane, and ThinWorld
has its own protocol:
<!-- Taken from Worlds.xml -->
<world Name="ThinWorld" ZoneFactory="ThinWorldZoneFactory" DisplayName="Thin World" Protocol="THIN"></world>
<world Name="Tzimtzlum" ZoneFactory="TzimtzlumWorldZoneFactory" DisplayName="Tzimtzlum" Plane="Tzimtzlum"></world>
Creating a zone factory
The IZoneFactory
for your world may also be relatively simple. The only method that you strictly need to implement is BuildZone
, although you may wish to override other methods from the interface.
Here is an example of a zone factory that loads the Yd Freehold for every "surface" zone, and fills the world map with jungle tiles:
namespace XRL.World.ZoneFactories {
public class MyWorldFactory : IZoneFactory {
public override Zone BuildZone(ZoneRequest Request) {
Zone zone = new Zone(80, 25);
zone.ZoneID = Request.ZoneID;
if (Request.IsWorldZone) {
zone.ForeachCell(delegate(Cell c) {
c.AddObject("TerrainJungle");
});
zone.DisplayName = "your world, world map";
return zone;
}
zone.loadMap("YdFreehold.rpm");
zone.DisplayName = "your personal world";
return zone;
}
public override void AfterBuildZone(Zone zone, ZoneManager zoneManager) {
ZoneManager.PaintWalls(zone);
ZoneManager.PaintWater(zone);
}
}
}
With this (and the world blueprint defined in Worlds.xml
) you now have a fully functional world! You can verify this by teleporting to the world, e.g. with goto:MyWorld.40.12.1.1.10
.
Using cells
Currently, our example zone factory is generating all zones directly in the BuildZone
function. This is simple, but can become unwieldy if we need to start generating many zones across a large area.
Instead we can define cell and zone blueprints in Worlds.xml
to make our work simpler. To do this, we must override two more functions from IZoneFactory
:
AddBlueprintsFor
, which adds zone blueprints appropriate to the zone that we're trying to generate.CanBuildZone
, which determines whether we build a zone directly usingBuildZone
or indirectly withGenerateZone
andAddBlueprintsFor
.
For our toy world, we'll expand our Worlds.xml
to the following:
<?xml version="1.0" encoding="utf-8" ?>
<worlds>
<world Name="MyWorld" ZoneFactory="MyWorldFactory" DisplayName="your personal world">
<cell Name="MyWorldCell">
<zone Level="10" x="1" y="1" Name="my joppa" IncludeStratumInZoneDisplay="true">
<map FileName="YdFreehold.rpm" />
<music Track="MehmetsMorning" />
</zone>
</cell>
</world>
</worlds>
And now our zone factory will look like this:
namespace XRL.World.ZoneFactories {
public class MyWorldFactory : IZoneFactory {
public override bool CanBuildZone(ZoneRequest Request) {
// Use BuildZone only for the world map; otherwise, we use GenerateZone
// and AddBlueprintsFor.
return Request.IsWorldZone;
}
public override Zone BuildZone(ZoneRequest Request) {
var zone = new Zone(80, 25);
zone.ZoneID = Request.ZoneID;
if (Request.IsWorldZone) {
zone.ForeachCell(delegate(Cell c) {
c.AddObject("TerrainJungle");
});
}
zone.DisplayName = "your personal world";
return zone;
}
public override void AddBlueprintsFor(ZoneRequest Request) {
// Normally we would use the fields of the ZoneRequest to figure out
// what cell blueprint we should get. In this case, we just resort
// to using the same blueprint for all cells.
var cellBlueprint = Blueprint.CellBlueprintsByName["MyWorldCell"];
var levelBlueprint = cellBlueprint.LevelBlueprint[1, 1, 10];
Request.Blueprints.Add(levelBlueprint);
}
public override void AfterBuildZone(Zone zone, ZoneManager zoneManager) {
ZoneManager.PaintWalls(zone);
ZoneManager.PaintWater(zone);
}
}
}
With this, we've deferred the zone building logic to the builders that are defined for the appropriate cell in Worlds.xml
.
Disabling the world map
You can disable world maps altogether by adding either the "inside"
or "SpecialUpMessage"
property to your zone. If you attempt to ascend in a zone marked with "inside", the game will simply attempt to go up to the next Z-level; this is used by interior zones and the Thin World, for example. "SpecialUpMessage" is used by Tzimtzlum to give a special message to the player when ascending.
You can set these zone properties in GenerateZone
and/or AfterBuildZone
:
// This gets called when creating a zone for which CanBuildZone == false
public override Zone GenerateZone(ZoneRequest Request, int Width, int Height) {
var zone = new Zone(Width, Height);
if (Request.ZoneID != null)
The.ZoneManager.SetZoneProperty(Request.ZoneID, "inside", "1");
return zone;
}
// This gets called after building zones for which CanBuildZone == true
public override void AfterBuildZone(Zone zone, ZoneManager zoneManager) {
zone.SetZoneProperty("inside", "1");
ZoneManager.PaintWalls(zone);
ZoneManager.PaintWater(zone);
}
Creating smaller world maps
Reason: The zero-width, zero-height zone hack in the code snippet below may have deficiencies that have not been observed yet. The game does not natively use world maps with dimensions < 80 x 25, so it's not currently clear what the best solution to this issue is.
In general, the game expects the world map to be 80 x 25. However, there are some tricks supported by the game that allow you to create smaller world maps. There are two primary technical considerations for supporting a smaller world: ensuring that players cannot cross the world boundary while (a) traversing the world map, and (b) while moving across the ground.
On the world map, you can use the InteriorVoid
object to define the boundaries of your world; these are the same objects used to define the boundaries of interior zones. Each InteriorVoid
serves as an opaque, impassable wall for the player.
It's somewhat trickier to prevent players from crossing the boundaries of the world on the ground. In order to prevent the player from reaching an adjacent zone, the game must receive a null
cell. The best known solution to this problem at the time of writing is to return a zero-width, zero-height zone when the player tries to cross the world boundary.
The following BuildZone
implementation demonstrates an example world that is defined on a 5x5 grid using the techniques described above.
using XRL.World.Parts;
namespace XRL.World.ZoneFactories {
public class MyWorldFactory : IZoneFactory {
public override bool CanBuildZone(ZoneRequest Request) {
// Use BuildZone for the world map and for zones outside of the map's
// boundaries.
return Request.IsWorldZone
|| Request.WorldX < 37
|| Request.WorldX > 43
|| Request.WorldY < 10
|| Request.WorldY > 14;
}
public override Zone BuildZone(ZoneRequest Request) {
var zone = new Zone(80, 25);
zone.ZoneID = Request.ZoneID;
zone.DisplayName = "your personal world";
if (Request.IsWorldZone) {
zone.ForeachCell(delegate(Cell c) {
c.AddObject("InteriorVoid");
Rocky.Paint(c);
});
for (int i = 37; i <= 43 ; i++) {
for (int j = 10; j <= 14; j++) {
var cell = zone.GetCell(i, j);
cell.Clear();
cell.AddObject("TerrainJungle");
}
}
return zone;
}
// For zones outside of the world's boundaries, we generate a zero-width,
// zero-height zone to prevent the player from entering.
return new Zone(0, 0);
}
// ...
}
}
This creates the following world map:
|