Skip to main content

Crate banished_terrain

Crate banished_terrain 

Source
Expand description

§banished-terrain

A port of Banished’s terrain: the map generator, the terrain mesh, the rivers, the ground height queries and the cell types. The same seed and settings give the same map as the original, bit for bit. Every stage was checked against output recorded from the original functions in the emulator, and against the heights stored in real save files.

PartWhat it does
GeneratorMakes the height grid from a seed and a size record, in five stages
Generator::surface_mapThe packed per-cell surface image (dirt, rock, light) that the terrain shader samples
TerrainMeshThe mesh: 32 × 32-cell tiles, each with 33 × 33 vertices, sharing one index list
HeightGridQueries on the saved heights: ray casts, the ground height and normal, the natural cell type
CellTypeThe type of a map cell: ground, rough, high, shallow or deep water
river_strip, water_meshThe triangle strip of a river, and the water surface

§Use it

[dependencies]
banished-terrain = { path = "crates/terrain" }
banished-formats = { path = "crates/formats" }   # to read the terrain profiles

§Generate a map

A terrain profile (Game/Terrain/Valleys.rsc or Game/Terrain/Mountains.rsc) holds one size record for each map size: small, medium and large. The generator takes a size record and a seed.

use banished_formats::game::terrain_generator;
use banished_formats::pkg::Archive;
use banished_terrain::{Generator, SizeRecord};

fn generate(archive: &Archive, seed: u32) -> banished_formats::Result<Generator> {
    let profile = terrain_generator(&archive.read_ref("Game/Terrain/Valleys.rsc:resource")?)?;
    let wire = profile.sizes[0]; // 0 small, 1 medium, 2 large
    let mut generator = Generator::new(SizeRecord::from_wire(wire), seed);
    generator.run(|stage, g| println!("stage {stage}: {} samples", g.grid.len()));
    Ok(generator)
}

After run, the generator holds:

  • size: the map’s size in cells (2n);
  • grid: (size + 1)² float heights;
  • heights(): the same heights as the game saves them, as i16 (the float × 256);
  • paths, lakes, points: the river paths and other records that the stages made.

surface_map() draws two more random numbers. Call it after run, as the game does.

§Ask the ground

use banished_terrain::{CellType, Generator, HeightGrid, TerrainMesh};

fn inspect(generator: &Generator) {
    let heights = generator.heights();
    let grid = HeightGrid { width: generator.width(), heights: &heights };

    // The ground height and normal at a map position (cells, centred on the map).
    let mut normal = [0.0; 3];
    let height = grid.ground(10.5, -3.25, &mut normal);

    // The natural type of every cell of the gameplay map (n × n cells in the middle of the
    // 2n-cell terrain), before anything is built.
    let n = generator.record.half_size() as usize;
    let water = (0..n * n)
        .filter(|&i| grid.natural_cell_type(n, i) == CellType::DEEP_WATER)
        .count();
    println!("height {height}, {water} cells of deep water");

    // The mesh that the renderer draws covers the whole terrain.
    let mesh = TerrainMesh::build(&grid, generator.size as usize);
    println!("{} vertices", mesh.vertices.len());
}

CellType::blocks_movement says whether citizens can walk on a cell. Shallow water, deep water and high ground block movement.

§Coordinates

  • The terrain of size 2n has 2n + 1 height samples on each side. n is the first value of the size record (SizeRecord::half_size): 256, 384 or 512 cells for the small, medium and large maps.
  • The playable gameplay map is the central n × n cells. The outer ring is scenery.
  • Positions are in cells, relative to the centre of the terrain: x and y run from -n to n.
  • A saved height is the generator’s float height × 256. In world space, z (up) is the saved height divided by 256.

§Tests

The tests for this crate are in crates/game/tests/:

  • terrain_emu.rs compares each generation stage, the cell types and the mesh with emulator output (make it with tools/emu/terrain_stages.py);
  • saves.rs regenerates the terrain of real saves from their seed and settings and compares it with the stored heights.

Both skip when their data is missing.

cargo test --release -p banished-game --test terrain_emu --test saves

§The API

The full API documentation is at https://tanepiper.github.io/unbanished/api/banished_terrain/index.html.

Structs§

CellType
The type of a map cell. Types 5 to 8 block movement (the mask 0x1e0); the others can be walked on (the mask 0xe1f). Types whose meaning is not confirmed have no name.
Generator
The map generator: it makes a terrain from a size record and a seed, in five stages.
HeightGrid
The heights of a map as a save stores them, for queries and for the mesh: width² values, row by row (see Generator::heights).
PathPoint
One point of a river path: a 2D position and five parameters from the size record (for example the river’s width).
SizeRecord
The settings of one map size in a terrain profile: 17 values, in the order the game keeps them in memory. The first value is n, half the terrain’s size in cells. The resource stores them in another order; SizeRecord::from_wire converts.
TerrainMesh
The terrain mesh: tiles of TILE_CELLS × TILE_CELLS cells, each with its own vertices, and one index list that every tile uses.
TerrainVertex
One vertex of the terrain mesh, 16 bytes, as the terrain shader reads it.
WaterMesh
The map’s water mesh: the strips of the rivers, then the surfaces of the lakes.

Constants§

TILE_CELLS
The number of cells on each side of a tile.
TILE_VERTS
The number of vertices on each side of a tile.

Functions§

river_strip
The strip of one path: vertices (x, y, u, v) and triangle indices.
tile_indices
Two triangles per cell; the diagonal alternates with (x + y) & 1.
water_mesh
The water mesh of a generated map. paths and lakes are the generator’s, heights is the map’s (size + 1)² height grid, and size is its size in cells.