Region map format
GET /roads/region/<id>/map returns a single
roads_cache.RegionMapSnapshot protobuf message. Terrain,
overlay, and claim_table are captured under one read lock
so claim indices in the overlay always resolve against the bundled table.
Schema: roads_cache.proto.
HTTP semantics
| Status | When | Body |
|---|---|---|
200 OK |
Region loaded and ready | Full RegionMapSnapshot protobuf (~280 MiB).
Response includes ETag header (same value as
message field etag). |
202 Accepted |
Region exists but grid not ready yet | Empty. Poll again after checking
/roads/health for state=READY. |
304 Not Modified |
Client sent matching If-None-Match |
Empty. Snapshot unchanged since last fetch. |
404 Not Found |
Unknown region id | Plain text: unknown region N |
503 Service Unavailable |
Roads cache disabled on host | Plain text: roads cache not enabled |
Always send Accept: application/x-protobuf.
Cache-Control: private is set on responses.
Protobuf message fields
| Field | Type | Meaning |
|---|---|---|
region |
uint32 |
BitCraft region id (path segment). |
generation |
uint64 |
Monotonic snapshot counter; bumps on each applied update batch. |
last_update_unix_ms |
int64 |
Wall-clock ms of the last grid mutation. |
origin_x, origin_z |
int32 |
World small-hex origin of this region’s overlay grid
(south-west corner). Same as
RegionsResponse.regions[].origin_x/z. |
claim_table |
repeated uint64 (packed) |
Index → claim entity id lookup. See below. |
neutral_claim_ids |
repeated uint64 (packed) |
Entity ids of claims marked neutral in upstream
claim_state. |
terrain |
bytes |
Super-hex terrain grid, little-endian u64 array. See below. |
overlay |
bytes |
Small-hex overlay grid, little-endian u32 array. See below. |
etag |
string |
SHA-256 hex digest of terrain || overlay ||
claim_table_le_bytes (used for conditional GET). |
World and region coordinates
BitCraft regions sit on a 5×5 grid. Constants (from the relay implementation):
| Constant | Value |
|---|---|
REGION_COUNT_SQRT | 5 |
CHUNKS_PER_SIDE | 80 chunks per region edge |
DEFAULT_CHUNK_SIZE | 96 small hexes per chunk |
REGION_SIDE | 7680 (= 80 × 96) small hexes per region edge |
SUPER_SIDE | 2560 (= 80 × 32) super hexes per region edge |
Region grid position from id (id is 1-based):
idx = region_id - 1
rx = idx mod 5
rz = idx div 5
origin_x = rx * 7680
origin_z = rz * 7680
Example: region 14 → rx=3, rz=2,
origin (23040, 15360).
Convert world small-hex coordinates to region-local overlay indices:
lx = world_x - origin_x // 0 … 7679
lz = world_z - origin_z // 0 … 7679
overlay_index = lz * 7680 + lx // row-major, u32 cell
Terrain grid (terrain bytes)
Layout: 2560 × 2560 super hexes,
row-major (super_z * SUPER_SIDE + super_x), each cell a
little-endian u64. Total size: 52 428 800 bytes
(~52.4 MiB).
Cell packing (bits in the u64, all little-endian on wire):
| Bits | Field | Type |
|---|---|---|
| 0–15 | elevation | i16 stored as u16 |
| 16–31 | original_elevation | i16 stored as u16 |
| 32–47 | water_level | i16 stored as u16 |
| 48–55 | water_body_type | u8 |
| 56–63 | (unused) | zero |
// Pack (matches relay pack_terrain):
packed = (elev & 0xFFFF)
| ((orig & 0xFFFF) << 16)
| ((water & 0xFFFF) << 32)
| ((wbt & 0xFF) << 48)
// Unpack:
elev = int16(packed & 0xFFFF)
orig = int16((packed >> 16) & 0xFFFF)
water = int16((packed >> 32) & 0xFFFF)
wbt = uint8((packed >> 48) & 0xFF)
Terrain is built from overworld-dimension
(dimension = 1) terrain_chunk_state rows.
Each chunk covers a 32×32 super-hex block inside the region grid.
Overlay grid (overlay bytes)
Layout: 7680 × 7680 small hexes,
row-major (lz * REGION_SIDE + lx), each cell a
little-endian u32. Total size: 235 929 600 bytes
(~225.9 MiB).
Cell packing (facet-masked — paving and claim index update independently):
| Bits | Field | Meaning |
|---|---|---|
| 0–15 | paving_type_id | 0 = unpaved; otherwise id from /roads/paving-types |
| 16–31 | claim_index | 0 = no claim tile; otherwise index into claim_table |
paving_type_id = cell & 0xFFFF
claim_index = (cell >> 16) & 0xFFFF
claim_entity_id = (claim_index == 0) ? 0 : claim_table[claim_index]
Overlay cells are populated from paved_tile_state and
claim_tile_state, joined through
location_state (world x/z) and
claim_state (neutral flag). Only tiles whose location
falls inside the region bounds appear in the grid.
Harvestable trees, ore, rocks, clay, and sand are not stored
in this overlay — look those up with
POST /roads/region/<id>/resources
using the same world small-hex (x, z).
claim_table and neutral_claim_ids
The overlay stores compact claim_index values (16 bits)
instead of full 64-bit entity ids. The parallel
claim_table array resolves them:
claim_table[0] = 0— sentinel (index 0 means “none”).claim_table[i]fori ≥ 1— claim entity id at that index.- Maximum 65535 distinct claims per region (16-bit index space).
neutral_claim_ids lists claim entity ids where upstream
claim_state.neutral is true. Use this to style or filter
neutral territory separately from player claims.
Reading a world tile (Python sketch)
import struct
from roads_cache_pb2 import RegionMapSnapshot # generated from roads_cache.proto
snap = RegionMapSnapshot()
snap.ParseFromString(open("region14.pb", "rb").read())
REGION_SIDE = 7680
SUPER_SIDE = 2560
def overlay_at_world(x: int, z: int) -> tuple[int, int, int]:
"""Return (paving_type_id, claim_index, claim_entity_id) at world small-hex (x, z)."""
lx = x - snap.origin_x
lz = z - snap.origin_z
if not (0 <= lx < REGION_SIDE and 0 <= lz < REGION_SIDE):
raise ValueError("out of region bounds")
idx = (lz * REGION_SIDE + lx) * 4
cell, = struct.unpack_from("<I", snap.overlay, idx)
paving = cell & 0xFFFF
claim_idx = (cell >> 16) & 0xFFFF
claim_id = 0 if claim_idx == 0 else snap.claim_table[claim_idx]
return paving, claim_idx, claim_id
def terrain_at_super(super_x: int, super_z: int) -> tuple[int, int, int, int]:
"""Return (elev, orig, water, wbt) at region-local super-hex."""
if not (0 <= super_x < SUPER_SIDE and 0 <= super_z < SUPER_SIDE):
raise ValueError("out of terrain bounds")
idx = (super_z * SUPER_SIDE + super_x) * 8
packed, = struct.unpack_from("<Q", snap.terrain, idx)
elev = struct.unpack("<h", struct.pack("<H", packed & 0xFFFF))[0]
orig = struct.unpack("<h", struct.pack("<H", (packed >> 16) & 0xFFFF))[0]
water = struct.unpack("<h", struct.pack("<H", (packed >> 32) & 0xFFFF))[0]
wbt = (packed >> 48) & 0xFF
return elev, orig, water, wbt
Conditional refresh with ETag
The server computes etag as SHA-256 hex over the raw
terrain bytes, then overlay bytes, then each
claim_table entry as little-endian u64. The same value is
returned in the HTTP ETag response header.
# First fetch — save body + ETag
curl -sH 'Accept: application/x-protobuf' -D headers.txt \
https://relay.bitcraftsync.app/roads/region/14/map -o region14.pb
ETAG=$(grep -i '^etag:' headers.txt | awk '{print $2}' | tr -d '\r')
# Later — skip download if unchanged
curl -sH 'Accept: application/x-protobuf' -H "If-None-Match: $ETAG" \
-D headers2.txt -o /dev/null -w '%{http_code}\n' \
https://relay.bitcraftsync.app/roads/region/14/map
# 304 → use cached region14.pb
# 200 → new body written, update local copy + ETag
Size and memory expectations
| Component | Dimensions | Raw size |
|---|---|---|
terrain | 2560×2560 × u64 LE | ~52.4 MiB |
overlay | 7680×7680 × u32 LE | ~225.9 MiB |
claim_table | variable (packed u64) | typically < 1 MiB |
| Total snapshot | protobuf-encoded | ~280 MiB per region |
On the relay host each live region holds one in-memory copy (~289 MiB steady state). Plan client storage and bandwidth accordingly; prefer ETag polling over full re-downloads.