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.
| Part | What it does |
|---|---|
Generator | Makes the height grid from a seed and a size record, in five stages |
Generator::surface_map | The packed per-cell surface image (dirt, rock, light) that the terrain shader samples |
TerrainMesh | The mesh: 32 × 32-cell tiles, each with 33 × 33 vertices, sharing one index list |
HeightGrid | Queries on the saved heights: ray casts, the ground height and normal, the natural cell type |
CellType | The type of a map cell: ground, rough, high, shallow or deep water |
river_strip, water_mesh | The 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, asi16(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
2nhas2n + 1height samples on each side.nis 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 × ncells. The outer ring is scenery. - Positions are in cells, relative to the centre of the terrain: x and y run from
-nton. - 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.rscompares each generation stage, the cell types and the mesh with emulator output (make it withtools/emu/terrain_stages.py);saves.rsregenerates 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.
§Related documents
Structs§
- Cell
Type - The type of a map cell. Types 5 to 8 block movement (the mask
0x1e0); the others can be walked on (the mask0xe1f). 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.
- Height
Grid - The heights of a map as a save stores them, for queries and for the mesh:
width²values, row by row (seeGenerator::heights). - Path
Point - One point of a river path: a 2D position and five parameters from the size record (for example the river’s width).
- Size
Record - 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_wireconverts. - Terrain
Mesh - The terrain mesh: tiles of
TILE_CELLS×TILE_CELLScells, each with its own vertices, and one index list that every tile uses. - Terrain
Vertex - One vertex of the terrain mesh, 16 bytes, as the terrain shader reads it.
- Water
Mesh - 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.
pathsandlakesare the generator’s,heightsis the map’s(size + 1)²height grid, andsizeis its size in cells.