Zone Management

Zones are named spatial regions within a world. Each zone has a position and size, and must fit entirely inside a single world partition. Zones let you organize your world into distinct areas — towns, forests, dungeons — and query which zone a player is in based on their coordinates.

Core Concepts

  • Zone: A named axis-aligned region with a position and size
  • ZoneManager: Accessed via world.Zones — registers zones, validates them against partitions, and provides spatial lookups
  • Partition constraint: A zone cannot be larger than the world's partition size and must fit entirely within one partition. Multiple zones can exist inside the same partition.

Note:

A zone's size is constrained by the partitioner config. If your partitioner is { width: 50000, height: 50000, depth: 50000 }, no zone can exceed 50000 in any dimension. If you need larger map regions, increase the partition size.

Zone Size and Partitions

The relationship between zones and partitions is strict:

World (3,300,000 x 1,900,000 x 50,000)
 └── Partitions (50,000 x 50,000 x 50,000 each)
      ├── Partition [0,0,0]
      │    ├── Zone "town_01"    (50000 x 50000 x 50000)  ← fills entire partition
      │    └── (no room for more)
      ├── Partition [1,0,0]
      │    ├── Zone "shop_district" (20000 x 20000 x 50000)  ← smaller than partition
      │    └── Zone "harbor"        (25000 x 20000 x 50000)  ← same partition, different area
      └── ...

Note:

The partition size in your config.yml defines the maximum zone size. Choose it based on the largest map region you need. If your biggest map is 100,000 x 100,000, set the partitioner to at least that.

Configuring Partition Size

The partitioner is configured in config.yml and determines the maximum zone dimensions:

altruist:
  game:
    worlds:
      partitioner: { width: 50000, height: 50000, depth: 50000 }
      items:
        - index: 0
          id: "main"
          size: { x: 3300000, y: 1900000, z: 50000 }

With this config, each zone can be at most 50,000 x 50,000 x 50,000 units.

IZone3D Interface

public interface IZone3D : IZone
{
    IntVector3 Position { get; }   // World-space origin of this zone
    IntVector3 Size { get; }       // Dimensions (width, height, depth)
}

public interface IZone :
{
    string Name { get; }
    bool IsActive { get; set; }
}

Registering Zones

Zones are registered through the world's Zones property (IZoneManager3D):

[Service]
public class WorldSetup
{
    private readonly IGameWorldOrganizer3D _worldOrganizer;

    public WorldSetup(IGameWorldOrganizer3D worldOrganizer)
    {
        _worldOrganizer = worldOrganizer;
    }

    [PostConstruct]
    public void Initialize()
    {
        var world = _worldOrganizer.GetWorld(0);
        if (world == null) return;

        // Register a 50000x50000x50000 town zone at the origin
        world.Zones.RegisterZone(new Zone3D(
            name: "town_01",
            position: new IntVector3(0, 0, 0),
            size: new IntVector3(50000, 50000, 50000)));

        // Register a forest zone in the next partition
        world.Zones.RegisterZone(new Zone3D(
            name: "forest_01",
            position: new IntVector3(50000, 0, 0),
            size: new IntVector3(50000, 50000, 50000)));

        // Register a smaller dungeon zone inside a partition
        world.Zones.RegisterZone(new Zone3D(
            name: "dungeon_01",
            position: new IntVector3(100000, 0, 0),
            size: new IntVector3(20000, 20000, 50000)));
    }
}

Note:

RegisterZone throws a ZoneValidationException if:

  • The zone's size exceeds the partition size in any dimension
  • The zone doesn't fit entirely inside a single partition (straddles a partition boundary)
  • A zone with the same name is already registered

What Makes a Zone "the Desert"?

A zone is a named bounding box — the framework handles spatial registration, validation, and lookups. Everything else is your game logic. The zone name is the bridge between the spatial region and your gameplay content.

For example, to make the region from (0, 0, 0) to (50000, 50000, 50000) a "desert":

[PostConstruct]
public void Initialize()
{
    var world = _worldOrganizer.GetWorld(0);

    // 1. Register the spatial region
    world.Zones.RegisterZone(new Zone3D(
        name: "desert",
        position: new IntVector3(0, 0, 0),
        size: new IntVector3(50000, 50000, 50000)));

    // 2. Spawn desert content at coordinates within this zone
    SpawnDesertEntities(world);
}

private async Task SpawnDesertEntities(IGameWorldManager3D world)
{
    // Spawn scorpions at desert coordinates
    await world.SpawnDynamicObject(new MonsterEntity(
        transform: Transform3D.From(new Vector3(10000, 0, 15000)),
        name: "Scorpion") { ZoneId = "desert" });

    // Spawn an NPC merchant
    await world.SpawnDynamicObject(new NpcEntity(
        transform: Transform3D.From(new Vector3(25000, 0, 25000)),
        name: "Desert Trader") { ZoneId = "desert" });

    // Spawn 50 cacti as static objects
    for (int i = 0; i < 50; i++)
    {
        var x = Random.Shared.Next(0, 50000);
        var z = Random.Shared.Next(0, 50000);
        await world.SpawnStaticObject(new AnonymousWorldObject3D(
            Transform3D.From(new Vector3(x, 0, z)),
            archetype: "cactus") { ZoneId = "desert" });
    }
}

Note:

The zone name is just a string you choose — "desert", "map_03", "pvp_arena", anything. It's the key you use to associate spawn data, client-side terrain, and gameplay rules with a region of the world. The framework doesn't care what the name means — it only uses it for lookups and identification.

Note:

Setting ZoneId = "desert" on spawned entities is not required by the framework, but it's useful for filtering (e.g., "destroy all entities in the desert zone") and for cleanup when deactivating zones.

Spatial Lookups

The zone manager provides built-in spatial queries — no custom lookup code needed:

// Find which zone a player is in
IZone3D? zone = world.Zones.FindZoneAt(
    (int)player.Transform.Position.X,
    (int)player.Transform.Position.Y,
    (int)player.Transform.Position.Z);

if (zone != null)
{
    Console.WriteLine($"Player is in zone: {zone.Name}");
}

// Find all zones overlapping a region
var nearbyZones = world.Zones.FindZonesInBounds(
    minX: 40000, minY: 0, minZ: 0,
    maxX: 60000, maxY: 50000, maxZ: 50000);

Note:

FindZoneAt only returns active zones (where IsActive == true). You can deactivate a zone by setting zone.IsActive = false — it will still be registered but invisible to spatial queries.

IZoneManager3D Interface

public interface IZoneManager3D : IZoneManager<IZone3D>
{
    IZone3D? FindZoneAt(int x, int y, int z);
    IEnumerable<IZone3D> FindZonesInBounds(
        int minX, int minY, int minZ,
        int maxX, int maxY, int maxZ);
}

public interface IZoneManager<TZone> where TZone : IZone
{
    TZone RegisterZone(TZone zone);
    TZone? GetZone(string name);
    bool RemoveZone(string name);
    IEnumerable<TZone> GetAllZones();
}
MethodDescription
RegisterZoneAdd a zone (validates against partition boundaries)
GetZoneGet a zone by name
RemoveZoneRemove a zone by name
GetAllZonesGet all registered zones
FindZoneAtFind the zone containing a world-space point
FindZonesInBoundsFind all zones overlapping a box region

Tracking Player Zone Transitions

Use FindZoneAt to detect when players move between zones:

[Service]
public class ZoneTracker
{
    private readonly Dictionary<string, string> _playerZones = new();
    private readonly IGameWorldOrganizer3D _worldOrganizer;

    public ZoneTracker(IGameWorldOrganizer3D worldOrganizer)
    {
        _worldOrganizer = worldOrganizer;
    }

    public void UpdatePlayerZone(string clientId, float x, float y, float z)
    {
        var world = _worldOrganizer.GetWorld(0);
        if (world == null) return;

        var zone = world.Zones.FindZoneAt((int)x, (int)y, (int)z);
        string newZoneName = zone?.Name ?? "";
        string oldZoneName = _playerZones.GetValueOrDefault(clientId, "");

        if (newZoneName != oldZoneName)
        {
            if (!string.IsNullOrEmpty(oldZoneName))
                OnPlayerLeftZone(clientId, oldZoneName);

            if (!string.IsNullOrEmpty(newZoneName))
                OnPlayerEnteredZone(clientId, newZoneName);

            _playerZones[clientId] = newZoneName;
        }
    }

    private void OnPlayerEnteredZone(string clientId, string zoneName)
    {
        // Activate spawns, send zone name to client, etc.
    }

    private void OnPlayerLeftZone(string clientId, string zoneName)
    {
        // Deactivate spawns if last player, cleanup, etc.
    }
}

Note:

Call UpdatePlayerZone from your movement handler or on a periodic tick. The framework does not automatically track which zone players are in — you drive the transitions.

Spawning a Player into a Zone

When a player logs in, their saved coordinates place them in the correct zone:

// Load player from database
var prefab = await _prefabs.Query<PlayerPrefab>()
    .Where(p => p.Character.StorageId == characterId)
    .IncludeAll()
    .FirstOrDefaultAsync();

prefab.ClientId = clientId;

// Enter the world — position from Character.X/Y/Z
var world = _worldOrganizer.GetWorld(0);
await prefab.InitializeRuntime(world);

// Determine which zone the player's position falls in
var zone = world.Zones.FindZoneAt(
    (int)prefab.Character.X,
    (int)prefab.Character.Y,
    (int)prefab.Character.Z);

if (zone != null)
    _zoneTracker.OnPlayerEnteredZone(clientId, zone.Name);

_visibilityTracker.RefreshObserver(clientId);

Note:

In a single-world setup, a player's saved X/Y/Z coordinates implicitly determine their zone. A player at (52300, 18400, 0) is in whichever zone's bounds contain that point — FindZoneAt resolves it automatically.

Multiple Zones Per Partition

Multiple smaller zones can share the same partition:

// Partition at (100000, 0, 0) is 50000x50000x50000
// Place two zones inside it:

world.Zones.RegisterZone(new Zone3D(
    name: "village",
    position: new IntVector3(100000, 0, 0),
    size: new IntVector3(25000, 25000, 50000)));

world.Zones.RegisterZone(new Zone3D(
    name: "farmland",
    position: new IntVector3(125000, 0, 0),
    size: new IntVector3(25000, 50000, 50000)));

Note:

Zones within the same partition can overlap — FindZoneAt returns the first match. If you need non-overlapping zones, ensure their coordinate ranges don't intersect.

2D Zone Manager

For 2D worlds, use IZoneManager2D with Zone2D and IntVector2:

var world2D = _worldOrganizer.GetWorld(0) as IGameWorldManager2D;

world2D.Zones.RegisterZone(new Zone2D(
    name: "arena",
    position: new IntVector2(0, 0),
    size: new IntVector2(5000, 5000)));

var zone = world2D.Zones.FindZoneAt(2500, 2500);

Calculating Zone Layout

Given a partition size and world size, here's how to plan your zone layout:

Partition size: 50,000 x 50,000 x 50,000
World size:     3,300,000 x 1,900,000 x 50,000

Partitions along X:  3,300,000 / 50,000 = 66
Partitions along Y:  1,900,000 / 50,000 = 38
Total partitions:    66 x 38 = 2,508

Max zone size:       50,000 x 50,000 x 50,000 (= partition size)
Max zones possible:  2,508+ (multiple per partition allowed)
Zone NamePositionSizePartition
town_01(0, 0, 0)(50000, 50000, 50000)[0,0,0]
forest_01(50000, 0, 0)(50000, 50000, 50000)[1,0,0]
dungeon_01(100000, 0, 0)(20000, 20000, 50000)[2,0,0]
arena(120000, 0, 0)(10000, 10000, 50000)[2,0,0]

Note:

dungeon_01 and arena share partition [2,0,0] because they're both small enough to fit. As long as each zone individually fits within one partition, multiple zones can coexist.