Skip to content
TAURION
12Movement & the World

Movement & the World

The hex map, pathfinding, speed, stamina and world geography.

In plain terms (for players)#

In Taurion you pilot a vehicle (the game calls each one a "character" — e.g. the starter ship is internally rv st but shown in the UI as Raider for Red, Barracuda for Blue, Scarab for Green). The world is one giant map made of six-sided hex tiles. To travel, you tap a destination and your client sends the game a list of waypoints (corner points of your route); your ship then walks tile-by-tile from one waypoint to the next.

A few rules shape every trip:

  • Bigger, heavier ships move slower; scouts (Finder / Octo / Flea) are the fastest. Speed is a number you can read in §6.
  • Mountains/walls (called obstacles on the map) block you completely, and so do buildings. Driving onto a tile where another ship already sits is allowed but very slow.
  • Each faction has a large starter zone around its home: your own faction travels through it three times faster, but rival factions cannot enter it at all.
  • You cannot teleport diagonally across the map: each leg of your route must run in a straight hex line. Your client's path-finder handles this for you.

Everything below is the precise, code-derived version of those rules, written for client developers. Player-facing display names (Trimideum, Raider, …) appear next to the internal codes throughout.


This document is the authoritative reference for how characters (vehicles) move across the Taurion world, how the hex world is laid out, what blocks movement, how speed is computed, and how waypoints are encoded into on-chain moves. It is derived directly from the Game State Processor (GSP) source, which is the single source of truth for consensus rules.

Jargon, once: GSP = Game State Processor, the program that every node runs to compute the canonical game state from on-chain moves. Consensus-relevant / consensus rule = a rule that affects that shared state, so all nodes must agree on it exactly. Axial coordinates = the (x, y) scheme used to address hex tiles. L1 (hex) distance = the number of single-tile steps between two tiles. Edge weight = the movement-point cost to step onto a given tile. Waypoint = one (x, y) corner of a planned route. Fitment = an equippable module (weapon/utility) that modifies a vehicle's stats.

All rules below are consensus-relevant (they affect game state) unless explicitly marked as client-side helpers.


1. The hex coordinate system#

Taurion uses a hexagonal grid expressed in axial coordinates (x, y) (proto/geometry.proto:28-32, hexagonal/coord.hpp:36-49). Internally each coordinate is a signed 16-bit integer (HexCoord::IntT = int16_t, hexagonal/coord.hpp:42).

  • A third "cube" coordinate z is derived as z = -x - y (hexagonal/coord.tpp:76-80).

  • The six neighbours of a tile are reached by adding the six principal direction vectors, in this exact enumeration order (hexagonal/coord.tpp:181-189):

    Index Delta (x, y) Meaning
    0 (+1, 0) East
    1 (-1, 0) West
    2 ( 0, +1) South-east
    3 ( 0, -1) North-west
    4 (+1, -1) North-east
    5 (-1, +1) South-west

    This order matters: it determines how the path finder breaks ties between paths of equal length (hexagonal/coord.tpp:179-180).

  • L1 (hex) distance between two tiles is (|Δx| + |Δy| + |Δz|) / 2 (hexagonal/coord.tpp:140-148). This is the number of single-tile steps on the shortest unobstructed path and is used throughout the game (enter radius, spawn radius, safe-zone radius, path-finding range).

Principal directions and waypoints#

A "principal direction" between two tiles exists only when the two tiles lie on a straight hex line, i.e. when one of Δx == 0, Δy == 0, or Δx + Δy == 0 (hexagonal/coord.tpp:117-138). Movement between consecutive waypoints must be along a principal direction — see §6.


2. World map size and shape#

The map is loaded from a precomputed obstacle bit-vector. Its extent (mapdata/basemap_tests.cpp:51-64, mapdata/tiledata.hpp:30-56):

Property Value Source
minY (axial) -4096 basemap_tests.cpp:54
maxY (axial) 4095 basemap_tests.cpp:55
X range at y = 0 -4096 .. 4095 basemap_tests.cpp:59-63
Total tile slots 67,108,864 (= 2^26) tiledata.hpp:56

The X range varies per row (the map is roughly hexagon/disc shaped, not a rectangle). For each y, minX[y] and maxX[y] define the valid X span (tiledata.hpp:38-41, basemap.tpp:50-59). BaseMap::IsOnMap returns false for any tile outside these per-row bounds (basemap.tpp:50-59).

numTiles = 67,108,864 is only an allocation/indexing constant; many of those slots are off-map and unused.

The raw map data files (obstacledata.dat.xz, regiondata.dat.xz) are too large for version control and must be fetched and placed in data/ before building (data/README.md). They are compiled into the binary as blobs (mapdata/tiledata.hpp:90-112).


3. Static obstacles (the base map)#

Each on-map tile is either passable or an obstacle, encoded as a single bit in blob_obstacles (mapdata/tiledata.hpp:93-100, basemap.tpp:61-73). BaseMap::IsPassable(c) returns false if the tile is off-map or its bit is 0 (basemap.tpp:61-73).

Base edge weight#

Path-finding and movement use an edge weight = the cost (in movement points) to step from one tile to a neighbour. The base map weight is (mapdata/basemap.tpp:75-82):

Condition (target tile to) Edge weight
to is passable 1000
to is an obstacle / off-map NO_CONNECTION (= 2^32 - 1, impassable)

So a normal tile costs 1000 movement points to enter (basemap.tpp:78). NO_CONNECTION = std::numeric_limits<uint32_t>::max() is the sentinel for "no path" (hexagonal/pathfinder.hpp:47-54). The weight depends only on the destination tile, never the source.


4. Safe zones and faction starter zones#

On top of the obstacle layer sit safe zones, defined in proto/roconfig/safezones.pb.text. Each zone is a filled L1 disc: a centre, a radius, and optionally a faction (proto/config.proto:464-479, mapdata/safezones.cpp:60-74). Every tile with L1 distance <= radius of the centre belongs to the zone (safezones.cpp:60-61). Zones may not overlap — the config loader CHECK-fails on overlap (safezones.cpp:62-67).

Two kinds exist:

  • Faction starter zones (faction: "r"|"g"|"b"): one per faction, radius 300 (safezones.pb.text:3-22).

    Faction Display Centre (x, y) Radius
    RED (r) Red (1960, -2601) 300
    GREEN (g) Green (-3472, 1824) 300
    BLUE (b) Blue (547, 2497) 300
  • Neutral no-combat zones: no faction set, radius 30, placed around all neutral / partner buildings (safezones.pb.text:24-148). There are exactly 23 of them.

Both kinds are no-combat zones (SafeZones::IsNoCombat, safezones.tpp:46-62) — that matters for combat, not movement, but is relevant to building placement (§9).

Starter-zone effect on movement (movement.tpp:24-40)#

MovementEdgeWeight(map, faction, from, to) modifies the base weight based on the destination tile's starter zone:

Destination to Moving faction Resulting edge weight
Not a starter zone any base weight (1000)
Starter zone of my faction matching base / 3 (= 333 for normal tiles)
Starter zone of another faction mismatched NO_CONNECTION (blocked)

So a faction's starter zone is a wall to outsiders but gives its owners 3× faster travel inside (integer division: 1000 / 3 = 333, movement.tpp:38). Note the modifier applies on the step into the starter tile; moving out of one's own starter zone onto a normal tile costs the normal 1000 (movement_tests.cpp:266-290).

The official web client mirrors these exact numbers: base 1000, safe zone 333, vehicle slowdown 8000, blocked = NO_CONNECTION.


5. Dynamic obstacles (buildings and vehicles)#

Beyond the static map there is a per-block dynamic obstacle layer (src/dynobstacles.hpp:42-123) holding all buildings and all vehicles. It is rebuilt from the database every block before movement is processed (src/dynobstacles.cpp:32-50, src/logic.cpp:101).

FullMovementEdgeWeight combines the base map weight with dynamic obstacles (src/movement.cpp:133-154):

Destination tile to Effect on edge weight
Covered by a building footprint NO_CONNECTION — fully blocked (movement.cpp:147-148)
Occupied by another vehicle base weight × 8 (movement.cpp:150-151)
Otherwise unchanged

MULTI_VEHICLE_SLOWDOWN = 8 (src/movement.hpp:43). So stepping onto a tile that already has a vehicle costs 1000 × 8 = 8000 points (or 333 × 8 inside a friendly starter zone). Vehicles are therefore passable but very slow to move through; buildings are never passable.

  • From and to must differ — the function CHECK-fails on from == to (movement.cpp:141). The moving vehicle is temporarily removed from the dynamic map before its own step is computed, so it never sees itself as an obstacle (MoveInDynObstacles, movement.cpp:352-369).
  • DynObstacles::IsFree(c) (no building and no vehicle) is used for building placement and spawn-tile selection, not for movement (dynobstacles.hpp:84-88).

Worked example from the test suite (movement_tests.cpp:243-264): with a base weight of 10 and a vehicle sitting on (0,0), stepping (1,0) → (0,0) costs 10 × 8 = 80; stepping away (0,0) → (1,0) costs the plain 10; a building at (123,0) makes (123,1) → (123,0) return NO_CONNECTION.


6. Movement speed#

6.1 The speed stat (milli-tiles per block)#

A vehicle's base movement speed is configured per vehicle type as speed, in milli-tiles per block (proto/config.proto:120-121). Because the base edge weight of a tile is 1000, speed is numerically "movement points accumulated per block" and a vehicle with speed = 1000 advances exactly one normal tile per block. A vehicle with speed = 4500 accumulates 4500 points/block, i.e. up to 4.5 normal tiles/block.

Speed is stored on the character proto as c.proto.speed() and is set during stat derivation from the vehicle config plus fitments (src/fitments.cpp:137, :237). It is exposed in game-state JSON as speed (src/gamestatejson.cpp:275).

Vehicle base speeds (all factions, proto/roconfig/items/vehicles_*.pb.text)

Speeds are identical across the three factions for equivalent classes (red/blue/ green share the same numbers; vehicles_others.pb.text). Display names from the official web client.

Code (per faction prefix rv/bv/gv) Red / Blue / Green display speed
*v st (starter) Raider / Barracuda / Scarab 4500
*v s Looter / Urchin / Mantis 4500
*v m Pillager / Crocodile / Centipede 3500
*v l Marauder / Moray / Millipede 2500
*v sc (scout) Finder / Octo / Flea 5500
*v mt (medium transport) Blood Carrier / Cobra / Horse 3500
*v lt (large transport) Bone Carrier / Trident / Ox 2500
*v vlt (very large transport) Skull Carrier / Hydra / Elephant 2000
*v sa (small attack) Devourer / Gorgon / Bee 4500
*v ma (medium attack) Exterminator / Siren / Wasp 3500
*v la (large attack) Devastator / Poseidon / Hornet 3000
*v vla (very large attack) Annihilator / Leviathan / Widow 2500

(Starter vehicles rv st/bv st/gv st are speed: 4500, vehicles_starter.pb.text:11,35,60.)

6.2 Fitment speed bonuses (Energy Enhancement / "turbo")#

The only fitments that change base speed are the Energy Enhancement line (internal * turbo), which add a percent StatModifier to speed (fitments.pb.text lf/mf/hf/vhf turbo; applied at fitments.cpp:213, :237):

Fitment code Display name Speed StatModifier
lf turbo Light Energy Enhancement +10%
mf turbo Medium Energy Enhancement +20%
hf turbo Heavy Energy Enhancement +30%
vhf turbo Very Heavy Energy Enhancement +40%

StatModifier math (src/modifier.hpp:79-91): `result = base + base*percent/100

  • absolute. **Modifiers from multiple fitments are summed first, then applied once** (not compounded) (fitments.cpp:165-167, :213). E.g. two vhf turbogive+80%, not 1.4 × 1.4`.

6.3 Combat slow effects (Temporal Disrupter / "retarder")#

Some weapons apply a runtime speed-reduction effect to enemy vehicles (a combat effect, not a fitment on the slowed vehicle). These are the Temporal Disrupter (* retarder) and Wide Temporal Disrupter (* longretard) weapons (fitments.pb.text lines ~263-465; display names "Light/…/Very Heavy Temporal Disrupter" / "… Wide Temporal Disrupter" in the official client). The two differ in how they hit:

  • * retarder has area: 3 but no range field, so it is an area-of-effect burst centred on the attacker itself — it slows every enemy within L1 distance 3 of the firing vehicle, with no individual target picked (fitments.pb.text:263-364; area/range semantics proto/combat.proto:96-111).
  • * longretard has range: 4 and area: 2, so it picks a target up to L1 distance 4 away and applies a smaller AoE (radius 2) around that target — hence "Wide" / longer-reach (fitments.pb.text:365-465).

The percent slow applied to each affected enemy:

Weapon code Display Speed effect on target
lf retarder Light Temporal Disrupter -15%
mf retarder Medium Temporal Disrupter -20%
hf retarder Heavy Temporal Disrupter -25%
vhf retarder Very Heavy Temporal Disrupter -30%
lf longretard Light Wide Temporal Disrupter -10%
mf longretard Medium Wide Temporal Disrupter -15%
hf longretard Heavy Wide Temporal Disrupter -20%
vhf longretard Very Heavy Wide Temporal Disrupter -25%

These set the character's effects.speed StatModifier, which is read every block in GetCharacterSpeed (src/movement.cpp:161-177).

6.4 Effective speed computation (movement.cpp:161-177)#

Each block, the effective speed is computed as:

  1. Start from base c.proto.speed() (vehicle + turbo fitments already baked in).
  2. Apply the combat effects.speed StatModifier (movement.cpp:167-168).
  3. Floor at 0: if the result is negative it becomes 0 — the vehicle does not move but stays "moving" with its waypoints intact (movement.cpp:169-170; test CombatEffectBelowZero, movement_tests.cpp:467-476).
  4. If a chosen speed cap is set, take min(effective, chosen_speed) (movement.cpp:172-173).

Example (movement_tests.cpp:455-465): base 10, effect -50%, chosen speed 2 → effective min(10 - 5, 2) = 2.

6.5 Chosen speed cap (chosen_speed)#

Players may cap a vehicle's speed below its maximum (useful to keep a fast vehicle in a slower convoy). It is stored as movement.chosen_speed (proto/movement.proto:42-48) and set via the speed move field (§7.4).

  • Valid range: 1 .. MAX_CHOSEN_SPEED where MAX_CHOSEN_SPEED = 1,000,000 (src/moveprocessor.hpp:59, src/moveprocessor.cpp:1396-1403).
  • speed: 0, negatives, non-integers, or values > 1,000,000 are rejected (moveprocessor.cpp:1385-1403; tests moveprocessor_tests.cpp:1154,1193-1201).
  • Setting a chosen speed higher than the actual speed has no effect (the min keeps the real speed) (movement_tests.cpp:433-442).
  • A chosen speed can only be set on a character that is currently moving (moveprocessor.cpp:1388-1394). The handler runs after waypoints, so a single move may set both wp and speed (moveprocessor.cpp:1868-1873).

Exposed in JSON as movement.chosenspeed (gamestatejson.cpp:115-116).


7. Issuing movement: the move (RPC) format#

7.1 Top-level move envelope#

A Taurion move is a JSON object wrapped as g: { tn: <movedata> }, where tn is the Taurion game id. Inside, character commands live under the c key (src/moveprocessor.cpp:175). c may be either a single object or an array of command objects (moveprocessor.cpp:177-192). Each command must carry an id field, which itself may be a single ID or an array of IDs to batch the same operation across several characters (moveprocessor.cpp:203-217).

A command is only applied if the character exists and is owned by the move's sender (moveprocessor.cpp:230-244).

Minimal structure:

{
  "g": {
    "tn": {
      "c": [
        { "id": 1, "wp": "<encoded-waypoints>" }
      ]
    }
  }
}

7.2 Setting waypoints — wp (moveprocessor.cpp:559-619, applied at 1453-1483)#

  • wp is a string: the base64-of-deflate encoded waypoint list (§8).
  • wp: null is the explicit stop command — clears all movement (moveprocessor.cpp:570-578; the official client sends this for "stop").
  • Rejected if: wp is not a string/null (:580-587); the character IsBusy (an ongoing operation like prospecting/construction) (:589-595); the character is inside a building (:597-604); decoding fails (:606-613).
  • Setting waypoints first stops any current movement and mining (StopCharacter + StopMining, moveprocessor.cpp:1464-1465).
  • An empty decoded list is treated as a stop (no waypoints set) (moveprocessor.cpp:1467-1468).
  • Waypoints are ignored if the vehicle's base speed is 0 (moveprocessor.cpp:1472-1478).

Example (from tests, moveprocessor_tests.cpp:1024-1034 — here the encoded string is shown decoded for clarity):

{"g":{"tn":{"c":{"id":1,"wp":"<encodes [{\"x\":-3,\"y\":4},{\"x\":5,\"y\":0}]>"}}}}

→ character 1 gets waypoints (-3,4) then (5,0).

7.3 Extending a path — wpx (moveprocessor.cpp:621-659, applied at 1485-1499)#

wpx appends additional waypoints to the end of the current list without restarting movement. It is valid only if the character is already moving (has non-empty waypoints) (moveprocessor.cpp:638-644). wp and wpx may appear in the same command; wp is processed first, then wpx (moveprocessor.cpp:1871-1872; test moveprocessor_tests.cpp:1124-1145).

7.4 Chosen-speed cap — speed (moveprocessor.cpp:1380-1409)#

Integer 1 .. 1,000,000. See §6.5. Example: {"id": 1, "wp": "<enc>", "speed": 1000} sets waypoints and caps speed to 1000 milli-tiles/block (moveprocessor_tests.cpp:1184-1186).

7.5 Enter / exit building — eb / xb (see §10)#

7.6 Processing order within one character command (moveprocessor.cpp:1847-1905)#

The order is fixed and lets several effects be combined in one move:

  1. send (transfer character)
  2. prospect
  3. change vehicle, then fitments
  4. start mining
  5. set waypoints (wp), then extend (wpx), then speed
  6. found building
  7. mobile refining
  8. drop (drop) then pickup (pu)
  9. enter building (eb) then exit building (xb)

8. Waypoint encoding (the wp / wpx string)#

Waypoints are transmitted as a compact, compressed string. The exact pipeline (src/movement.cpp:49-114, mirrored client-side in the official web client):

Encode: array of {x, y} → JSON string (no whitespace, e.g. [{"x":-3,"y":4},{"x":5,"y":0}]) → raw DEFLATE compress → base64.

Decode: base64 → raw inflate → parse JSON array of {x, y}.

Critical implementation details:

  • Compression uses Xaya's libxayautil CompressJson / UncompressJson (movement.cpp:58, :80). This is raw DEFLATE, windowBits = -15, i.e. no zlib header. In JavaScript you must use pako.deflateRaw / pako.inflateRaw, not the zlib-wrapped variants.
  • Coordinates serialise as {"x": <int>, "y": <int>} (CoordToJson/CoordFromJson).
  • DoS / consensus guard: decoding is limited to MAX_WAYPOINT_SIZE = 1 MiB (1 << 20) of uncompressed JSON, and to a maximum JSON nesting depth of 3 (movement.cpp:45, :80-81). A path that compresses fine but decompresses to more than 1 MiB is rejected. This is consensus-relevant: it can make some very large waypoint moves invalid. "Normal paths across the whole map are only about 3-4 KiB" (movement.cpp:43).
  • EncodeWaypoints itself also fails if the serialised JSON exceeds 1 MiB (movement.cpp:59-65; test TooLargeForEncoding, movement_tests.cpp:95-107).
  • Decode rejects: non-array JSON, entries that are not {x:int, y:int}, extra nested fields exceeding depth 3, and oversized payloads (movement.cpp:90-110; test InvalidDataForDecode, movement_tests.cpp:109-136). An empty array is valid and round-trips (movement_tests.cpp:81-93).

Helper RPCs (non-state, client convenience)#

The GSP exposes non-state RPCs (no blockchain state needed; served by NonStateRpcServer in pxrpcserver.cpp):

  • encodewaypoints(wp) — encodes a raw [{x,y},…] array to the string (pxrpcserver.cpp:361-384; stub rpc-stubs/nonstate.json:24-27). Returns the encoded string.
  • findpath(exbuildings, faction, l1range, source, target) — runs the same path-finder the GSP uses and returns the route (pxrpcserver.cpp:233-359; stub nonstate.json:12). Output object: { "dist": <total weight>, "wp": [ {x,y}, … ], "encoded": "<string>" } (pxrpcserver.cpp:353-356). It compacts collinear steps into the minimal set of waypoints (one per direction change) (pxrpcserver.cpp:327-345).
    • exbuildings is an array of building IDs to ignore as obstacles (so you can path into a building you intend to enter) (pxrpcserver.cpp:276-312).
    • l1range bounds the search radius (DoS guard) — only tiles within that L1 distance of the target are considered (pathfinder.hpp:102-117).
    • Returns FINDPATH_NO_CONNECTION if no route exists within range (pxrpcserver.cpp:322-325).
    • The path finder uses Dijkstra with the exact movement edge weights, incl. starter-zone and vehicle-slowdown rules (pxrpcserver.cpp:298-318).

The official web client either calls findpath/encodewaypoints over RPC or computes paths locally with an identical WASM/JS pathfinder and pako.


9. How movement is processed each block#

Movement runs once per block, after moves are applied and after combat HP/mining, before building-entry and target selection (src/logic.cpp:101-121):

ProcessAll(moves) → HP updates → ongoings → mining → ProcessAllMovement
→ ProcessEnterBuildings → FindCombatTargets

For each moving character (queried via QueryMoving, movement.cpp:329-330), CharacterMovement runs (movement.cpp:251-322):

  1. Compute effective speed (§6.4). If 0, do nothing this block (movement.cpp:263-265).
  2. Accumulate movement points: partial_step += speed (movement.cpp:272-273). partial_step is volatile state carried between blocks (proto/movement.proto:57-69), exposed as movement.partialstep (gamestatejson.cpp:106-107).
  3. Loop, stepping one tile at a time toward the first waypoint:
    • If the current position already equals the next waypoint, that waypoint is popped; duplicate / already-reached waypoints are silently skipped (movement.cpp:286-302; test DuplicateWaypoints, movement_tests.cpp:478-491). When the list empties, movement stops entirely (StopCharacter, movement.cpp:294-298).
    • The direction to the next waypoint must be a principal direction; otherwise movement is stopped and the remaining waypoints discarded (movement.cpp:304-317; test WaypointsNotInPrincipalDirection, movement_tests.cpp:493-500). Clients must therefore only emit waypoints that are pairwise collinear — this is what findpath guarantees.
    • Each single step costs the edge weight of the target tile (§3-§5). The step happens only if partial_step >= edgeWeight; then partial_step -= edgeWeight and the position advances (movement.cpp:239-248). Several tiles can be crossed in one block if enough points accumulated (movement_tests.cpp:412-420).
    • When partial_step is insufficient for the next step, processing pauses until the next block (movement.cpp:239-243).

Multiple vehicles in one block (movement_tests.cpp:620-652)#

Movement is processed ordered by character ID (QueryMoving). The dynamic obstacle map is updated live as each vehicle moves, so if two vehicles aim for the same tile in the same block, the lower ID wins the tile and the higher ID is blocked there for that step.


10. Blocked steps and the retry mechanism#

When the next step is into an obstacle (building, off-map, enemy starter zone, or — transiently — a vehicle), the step fails (movement.cpp:198-226):

  • partial_step is reset to 0 and a blocked_turns counter is incremented (movement.cpp:211-213), exposed as movement.blockedturns (gamestatejson.cpp:108-109).
  • The character keeps retrying the same step each block, because the blocker might just be a passing vehicle (movement.cpp:206-209).
  • If blocked_turns exceeds params.blocked_step_retries, movement is cancelled entirely (StopCharacter) (movement.cpp:217-223).
  • As soon as the way is clear again (even if a full step still isn't affordable), blocked_turns is reset to 0 (movement.cpp:228-237).

blocked_step_retries = 25 (mainnet and testnet — only params.pb.text:6 sets it, and the testnet merge does not override it). Per the proto comment, this is the number of retries, so movement is cancelled after N+1 = 26 consecutive blocked turns (proto/config.proto:540-546; test logic movement_tests.cpp:519-559).

Note (movement_tests.cpp:502-517): after a successful step that uses up all points, the engine already tries (and may fail) the following step in the same block, so the blocked counter can be incremented "for free" on the same block the vehicle reaches the obstacle.


11. Buildings: footprints and enter / exit rules#

11.1 Building footprints block movement#

A building occupies a set of tiles defined by shape_tiles in its config, rotated by the building's shape_trafo.rotation_steps (in 60° hex steps) and translated to the building's centre (src/buildings.cpp:36-53, GetBuildingShape). Every tile in that footprint is an absolute movement blocker (movement.cpp:147-148).

Footprint sizes vary widely (counts from proto/roconfig/buildings/*.pb.text):

Building (examples) Footprint tiles enter_radius
r_r / g_r / b_r (refinery hut, smallest) 9-13 8
*_rt (refinery/tower) 19 6
*_ss (space station, largest player build) 699-1149 150
ancient1/2/3 352-378 54-58
obelisk1/2/3 970-1075 31-35

enter_radius per building type ranges from 6 (*_rt) up to 150 (*_ss); ancient/obelisk structures are larger. The full table is in the config; representative values: most red/green/blue named buildings sit in the 6-16 range. test building uses enter_radius: 5.

11.2 Queuing entry — eb (moveprocessor.cpp:661-727, applied 1501-1509)#

eb: <buildingId> sets an intent to enter; it does not move the character. Rules:

  • eb: null cancels a pending enter intent (moveprocessor.cpp:681-689).
  • Rejected if the character is already in a building (:670-677), the building doesn't exist (:700-707), or the building's faction is neither ANCIENT nor the character's own faction (:709-720). I.e. anyone may enter ancient buildings; otherwise only same-faction buildings.
  • Entry intent is not blocked by IsBusy — it just sets a flag (moveprocessor.cpp:1891-1892).
  • Stored on the character as enterbuilding (the target building ID), exposed in JSON when set (gamestatejson.cpp:271-272).

11.3 Actually entering (ProcessEnterBuildings, buildings.cpp:178-233)#

Each block, after movement, every character with an enter intent is checked:

  • Skipped if IsBusy (buildings.cpp:192-197).
  • If the target building was destroyed meanwhile, the intent is cleared (buildings.cpp:204-213).
  • The character enters only if the L1 distance from its position to the building centre is <= enter_radius of that building type (buildings.cpp:215-221). Otherwise it keeps the intent and keeps moving toward it on later blocks.
  • On entry (EnterBuilding, buildings.cpp:167-176): the vehicle is removed from the dynamic obstacle map, its position is replaced by inbuilding = <buildingId>, its combat target is cleared, movement and mining are stopped, and the enter intent is cleared.

Because entry is checked against the centre with a generous radius, a vehicle typically enters automatically as soon as it is "near enough", and a combined { "wp": "<path to building>", "eb": <id> } move (the official client's "move and enter") makes the vehicle path in and enter as soon as it arrives.

11.4 While inside a building#

A character with inbuilding set has no map position (gamestatejson.cpp:266-269) and cannot receive waypoints (wp is rejected, moveprocessor.cpp:597-604). It can still do in-building actions (refit, trade, construct, drop/pickup).

11.5 Exiting — xb (moveprocessor.cpp:729-767, applied 1511-1518)#

xb: {} (an empty object) exits the building immediately. Rules:

  • The value must be an empty object — any non-object, or an object with fields, is rejected (moveprocessor.cpp:734-744; tests moveprocessor_tests.cpp:2125-2156).
  • Rejected if the character is IsBusy (:746-752) or not in any building (:754-760).
  • On exit (LeaveBuilding, buildings.cpp:235-252): the game picks a spawn location near the building centre within its enter_radius, sets the vehicle's position there, and re-adds it to the dynamic obstacle map.

11.6 Spawn-location selection on exit / spawn (spawn.cpp:39-114)#

ChooseSpawnLocation(centre, radius, …):

  1. Pick a uniformly random tile within L1 radius of the centre (rejection sampling, RandomSpawnLocation, spawn.cpp:39-73).
  2. From that point, scan outward in growing L1 rings and return the first tile that is on the map, passable, and free of dynamic obstacles (no building, no vehicle) (spawn.cpp:84-113). Vehicles are skipped even though they are technically passable, so the exiting vehicle does not land on another (spawn.cpp:100-104).

New characters are spawned into a building (not onto the map): the spawn building per faction is configured in params.spawn_areas (spawn.cpp:147-151, params.pb.text:33-47):

Faction Spawn building_id
RED (r) 6
GREEN (g) 4
BLUE (b) 5

(Newly spawned vehicles: RED rv st, GREEN gv st, BLUE bv st, with one lf gun fitted — spawn.cpp:126-143.)


For a character, the game-state JSON includes (gamestatejson.cpp:255-281):

Field Meaning
position {x, y} — present only when not in a building (:266-269)
inbuilding building ID — present only when inside a building (:266-267)
enterbuilding building ID of a pending enter intent, if any (:271-272)
speed current effective base speed (vehicle + fitments), milli-tiles/block (:275)
movement.waypoints array of remaining {x,y} waypoints (first = current target) (:118-122)
movement.chosenspeed speed cap, if set (:115-116)
movement.partialstep accumulated movement points toward the next tile (:106-107)
movement.blockedturns consecutive blocked-step count (:108-109)

The movement sub-object is omitted entirely when empty (gamestatejson.cpp:279-281).


13. Mainnet vs testnet / regtest differences#

  • Map data, edge weights, obstacle layer, safe zones, vehicle speeds, fitment modifiers, MULTI_VEHICLE_SLOWDOWN, MAX_CHOSEN_SPEED, waypoint encoding / size limits, and blocked_step_retries (25) are identical across mainnet (Polygon) and testnet — testnet.pb.text is an empty merge (proto/roconfig/testnet.pb.text:1) and the movement-relevant params are not overridden in test_params.pb.text.
  • test_safezones.pb.text exists as a regtest-only variant of the safe-zone layout; tests run against Chain::REGTEST (basemap_tests.cpp:46). The three faction starter zones (radius 300) and neutral zones (radius 30) above are the production layout (safezones.pb.text).
  • Spawn building IDs (RED 6 / GREEN 4 / BLUE 5) come from initialised ancient buildings; the same params.spawn_areas are used on all chains.
  • Path-finding l1range is supplied by the caller (client), not a chain param.

Open questions#

  1. Exact minX[]/maxX[] per row. The precise map silhouette lives in the compiled obstacledata.dat blob (not in version control, data/README.md); only the bounding box (Y ∈ [-4096, 4095], X ∈ [-4096, 4095] at the centre row) is verifiable from basemap_tests.cpp. The full per-row X bounds and the obstacle bitmap itself could not be enumerated from source alone.
  2. Per-building shape_trafo used in production. Footprint shapes (shape_tiles) and enter_radius are in config, but each placed building's actual rotation comes from initial_buildings / player foundation moves; the resulting absolute footprints are not enumerated here.
  3. obstacle/passable semantics of partner safe-zone interiors for movement. No-combat zones affect combat and building placement (buildings.cpp:76-80), but do not by themselves change movement edge weight (only faction starter zones do, §4). Confirmed from code; flagged only because the naming ("safe zone") might suggest movement effects that do not exist.
  4. Speed-affecting items (resolved by exhaustive scan). Every speed: modifier in fitments.pb.text was enumerated: the only positive fitment speed modifiers are the four * turbo lines (+10/+20/+30/+40%), and the only negative ones are combat effects on * retarder (-15/-20/-25/-30%) and * longretard (-10/-15/-20/-25%). No other fungible item carries a speed StatModifier, so §6.2/§6.3 are complete for fitments. (Vehicle base speeds themselves are listed in §6.1.)