OP Wiki

Plant fields

Plant behaviour is defined per species by a rule file (plain text) under $GameRoot/plantrules/. Which files belong to a zone is the DB mapping plantrules(idx, plantrule, rulesetid) → zones.plantruleset; loading and merging is PlantRuleLoader.cs, and the field-to-object mapping is PlantRule.cs.

File format

One key=value per line; //-style comments are stripped. Values use GenXY token characters (GenxyReader.cs — the first character selects type and number base):

TokenParsed asExample
nint, decimaln125, n-1
Nint[], decimal, comma-separatedN5,5,5,5
fdouble, decimalf0.1
$string$prismocitae
iint, hexiFF
Llong, hexL100
4int[], hex41,2,3 → [1, 2, 3]

⚠ The "bare array" trap

Files also contain values like blockingHeight=40,3,3,4 with no explicit token. The parser does not special-case these: the leading 4 is consumed as the IntegerArray (hex) token, and the rest is parsed as hex. So:

blockingHeight=40,3,3,4   →  token '4' + hex array "0,3,3,4"  →  [0, 3, 3, 4]
blockingHeight=N0,8,11,14 →  token 'N' + decimal array         →  [0, 8, 11, 14]

Both spellings end up as int[], and the leading 0 is what the 4 "swallows". This is why every bare array in the shipped files starts with 4 — it is a convention that keeps the first element at the intended value. When writing new rule files, use the N prefix explicitly; values ≥ 10 in a bare array would be parsed as hex and silently change meaning.

Inheritance

A source=$other_rule.txt key loads other_rule.txt first and applies the current file's keys as overrides (PlantRuleLoader.LoadRuleByName). Example: irontree_t2.txt is just:

source=$irontree_lo.txt
fruitAmount=n125

Field reference

All fields read from the merged settings dictionary (PlantRule.cs). Missing keys fall back to the default shown.

KeyType / defaultConsumed byEffect
indexint (0)PlantRule.TypeThe PlantType enum value for this species (1=GrassA … 19=Devrinol). Also the PlantInfo.type byte stored per tile.
namestringclient info payloadDisplay name.
fertilityint (0)PlantRuleExtensions.GetWinnerPlantTypeBasedOnFertilityWeight in the weighted draw that picks a species for a tile with no plant neighbours. 0 = never spawns.
spreadingint (0)GetSpreadingBasedWinnerPlantTypeWeight multiplier when a tile has neighbours: candidate weight = neighbourCount × spreading. 0 = never wins the spreading draw.
killDistanceint (0)NatureCube.CheckKillDistanceMinimum distance from any existing plant of the same type; 0/negative = no limit.
growRateint (0)GrowPlantOnTileNumber of cube ticks the PlantInfo.time byte must accumulate before the state machine rolls. Higher = slower.
slope / minSlopeint (0)ValidateTileAllowed slope band of the derived slope layer. Outside the band the plant is cleaned.
allowedAltitudeLow/Highint (0)ValidateTileAltitude band, compared against Altitude / 4 (the /4 matches client coordinates). Scaled by the zone's plantaltitudescale.
allowedWaterLevelLow/Highint (0)ValidateTileBand for altitude/4 − waterLevel/4 — creates coastal vegetation. Also altitude-scaled.
allowedTerrainTypesint[] (empty)ValidateTileAllowed groundType bytes (see Zone files). Empty = nowhere. Skipped entirely when allowedOnNonNatural is set.
fruitingStateint (−1)GrowPlantOnTileFirst state that produces fruit. −1 ⇒ NotFruiting (never harvestable).
fruitDefinitionint (−1)harvestingItem definition of the fruit.
fruitMaterialNamestringdisplayFruit name (e.g. $prismocitae).
fruitAmountbyte (0), clamped 0–255GrowPlantOnTileMax fruit per plant. First entry into the fruiting state fills 5–15% of it; each later growth tick adds 15–25%, capped at 100%.
healthbyte[] per stateGrowPlantOnTile, combatHit points per growth state. On state change the current health ratio is carried over into the new state.
blockingHeightint[] per stateGrowPlantOnTile, spawn countingBlocking height per state; >0 sets the Plant bit in the block layer (walkable=false) and makes the plant count toward the zone fertility target.
maxAmountint (−1)SpawnPlantsPer-zone cap on the number of this species; −1 = unlimited.
playerSeededbool (0)GetNewPlantRule, fertility drawExcluded from all natural spawns; only exists where a player planted it.
damageScaledouble (1.0)plant combatIncoming damage multiplier.
onlyOnUnprotectedZonebool (0)spawn validationOnly grows where the zone is unprotected.
allowedOnNonNaturalbool (0)ValidateTileSkips the control-bit, terrain-type, and slope checks (grows on "artificial" tiles).
placesConcretebool (0)GrowPlantOnTile (death)On death, clears the ConcreteA/B control bits under the tile.
state_Nint[]GetNextStateNext-state choices for state N — each element is picked uniformly at random.
action_Nint[]GetNextStateNext PlantType choices for state N (uniform). An action value of 0 (NotDefined) means death.

Consistency is enforced at load (CheckConsistency): len(health) == len(blockingHeight) == number of states.

The growth state machine

Each 32×32 cube tick, for every plant tile (GrowPlantOnTile):

  1. If time < growRate: time++, done.
  2. Else: time = 0, roll state_N and action_N for the current state:
    • action == 0 → the plant dies (kill signal type:0, state:1 for the client); if placesConcrete, clear concrete under the tile.
    • else → type = action, state = next, health = healthRatio × Health[newState] (damage ratio preserved).
    • Fruit: if newState == fruitingState, material = fruitAmount × uniform(0.05, 0.15); if newState > fruitingState, material += fruitAmount × uniform(0.15, 0.25) (capped at fruitAmount); otherwise material = 0.
    • Block height = blockingHeight[newState]; the block layer's Plant bit follows height > 0.

Worked example — grass_a.txt

index=n1            → GrassA
fertility=n1        → 1/… weight in the no-neighbour draw
spreading=n2        → cluster weight = 2 × neighbours
growRate=n1         → rolls a state every cube tick (fast)
maxAmount=n190      → at most 190 per cube
health=N5,5,5,5     → 5 HP in every state
blockingHeight=40,0,0,0 → parsed as [0,0,0,0]: grass NEVER blocks
fruitingState=n-1   → NotFruiting: grass is never harvestable
state_0=40,0,1      → [0,0,1]: 2/3 stay in state 0, 1/3 advance
state_1=41,1,2      → [1,1,2]
state_2=42,2,3      → [2,2,3]
state_3=43          → [3]
action_3=N1,1,1,0   → in the final state: 3/4 stay, 1/4 die

So a grass tile: advances roughly one state every 3 ticks on average, and in the last state has a 25% chance per tick of dying and re-seeding — an ever-turnover cover species with no fruit.

Worked example — wall.txt (12 states, N-prefixed)

blockingHeight=N0,0,2,3,4,7,9,10,10,13,13,14, health=N20,60,80,…,255, state_7=N7,7,8,8,6 (can regress a state), playerSeeded=n1, onlyOnUnprotectedZone=n1, maxAmount=n15: a player-planted fortification that only exists in unprotected zones, capped at 15 per cube, that grows (and occasionally shrinks) through 12 states up to height 14 and 255 HP.

Where the per-tile state lives

The runtime state per tile is the 7-byte PlantInfo struct in the zone's plant layer file (type, state, time, spawn, health, material, groundType) — see Zone files. The spawn byte is a per-tile spawn probability baked into the zone file — 0 marks a sterile tile (plants on it are cleared during validation, it never sprouts); any other value means a plant-regen attempt on that tile passes a random gate with chance ≈ spawn/256. The server never writes the byte. Live data: every tile of plants.0106.bin carries 0x83 (≈ 51%).

Client payload

PlantRuleExtensions.GetPlantInfoForClient sends, per species: index, mineral (= fruitDefinition), name, blocks (blockingHeight), health.