
Farming Module: One Pattern, Three Worlds — and Zero Storage of Its Own
Overview
This is Part 3 of One Pattern, Three Worlds — the curriculum arc's payoff. The NPC state machines module proved the shape on combat AI: in Verse, concurrency IS the state machine — dispatch on the current state, race that state's own timer against its triggers, loop. The sidekick companion module carried it into social AI. The farming module is the third world — simulation: a plot's growth cycle (seed → sprout → grow → harvest) is the same race-per-state machine. Guard → companion → plant: the pattern generalizes to anything with a timed, tendable, interruptible lifecycle, not just NPC behavior. Compile-verified as a set on the real UEFN compiler, Release-41.20.
The design headline is what this module doesn't own: storage. A island save-slot rule landed — an island gets at most two persistent weak_maps total, and a published weak_map's data shape is frozen forever — so every system consolidates its persisted per-player fields into ONE shared player_data struct, never a map of its own. farm_progression was reshaped to comply completely: farm_persistent_data is a plain embeddable struct, every function is a pure record-in/record-out transform, and a progression_sink interface is the seam to whatever storage an integrating project supplies. Search the files: zero weak_map declarations across all five files — the only mentions are comments citing the save-slot rule.
| File | Responsibility |
|---|---|
farm_types.verse |
crop_def + the Registry table, plot triggers/blackboard, the harvest_sink/progression_sink boundaries |
farm_growth.verse |
the growth FSM — pure logic, no devices |
farm_plot.verse |
the scene-graph plot component: one state-dependent interact button + the growth-stage scale swap |
farm_progression.verse |
level / XP / crop-unlock gate — pure transforms, owns NO storage |
island_garden.verse |
worked example — discovers plot meshes, cycles two crops, wires both boundaries |
It's a foundational flagship: the growing core gets the full read below; persistence appears only as the embeddable-struct pattern — the deep save-structure treatment belongs to the dedicated Persistence & Save Data module coming later in this series.
The module, file by file
farm_types.verse — crops as data, plus the two boundaries
The FSM's mood is a five-state enum — the NPC state-machine module's guard_state shape, third domain to reuse it:
plot_growth_state<public> := enum:
Empty
Seeded
Sprout
Mature
Wilted
A crop is a Registry() row, not a class: SproutSeconds/GrowSeconds/MatureSeconds timers, YieldId/YieldAmount plus a reduced WiltedYieldAmount, Regrows, and a RequiredLevel gate. Adding a third crop is one Registry row and one rotation entry in the garden — zero new classes, zero new FSM states, zero new gating code. (Registry is a function rebuilt per call: a module-scope archetype constant of struct type fails Script error 3512.)
Two interfaces bound the module. harvest_sink (GrantYield) is the ONE place a harvest becomes real — deliberately undecorated, since concrete implementations call native grant APIs that carry no_rollback. And the progression seam:
progression_sink<public> := interface:
# `<transacts>`: farm_plot.verse's HandlePlant calls this from
# inside an `if:` binder line, which demands a transacts-safe
# callee (confirmed the hard way: Script error 3512 no_rollback
# when this was left undecorated).
IsCropUnlocked<public>(Player : player, Crop : crop_def)<transacts> : logic
AwardXP<public>(Player : player, Amount : int) : void
That in-source comment was earned on the real compiler — and the <transacts> requirement propagates through the whole call chain (a Gotcha below).
farm_growth.verse — the same FSM, third world
Pure Verse, no devices, no tick polling. RunPlotFSM dispatches on Blackboard.CurrentState and races each state's own timer and triggers — read it beside RunGuardFSM and RunCompanionFSM: same shape, three domains. Empty just awaits PlantEvent — nothing planted, nothing to time. Seeded/Sprout each race a growth timer TendEvent can shorten (no harvest leg: you can't harvest what isn't Mature). Mature races the guard's exact Investigate/Chase two-leg shape — a timeout leg against a signal leg:
race:
block:
# Neglect: no harvest arrived before the
# crop's own patience ran out.
Sleep(Crop.MatureSeconds)
set Blackboard.CurrentState = farm_types.plot_growth_state.Wilted
block:
Harvester := Triggers.HarvestEvent.Await()
Sink.GrantYield(Harvester, Crop, Crop.YieldAmount)
if (Crop.Regrows?):
set Blackboard.CurrentState = farm_types.plot_growth_state.Seeded
else:
set Blackboard.CropId = ""
set Blackboard.CurrentState = farm_types.plot_growth_state.Empty
"What if they neglect it?" is answered in code: neglect isn't a special case, just the timeout leg winning the same race every transition uses. Wilted mirrors the companion's Scared mood — one legal way out, a single await, no race.
The one piece with no guard/companion precedent: TendEvent must shorten the current state's timer without transitioning. RaceGrowthTimer makes the concurrency itself the accelerator — no separate "remaining" field to keep in sync:
RaceGrowthTimer(Triggers : farm_types.plot_triggers, Duration : float, Accelerate : float)<suspends> : void =
var Remaining : float = Duration
loop:
var Elapsed : logic = false
race:
block:
Sleep(Remaining)
set Elapsed = true
block:
Triggers.TendEvent.Await()
if (Remaining - Accelerate > 0.0):
set Remaining -= Accelerate
else:
set Remaining = 0.0
if (Elapsed?):
break
A tail-recursive version looks equivalent and was tried first — it's wrong: the original Sleep leg keeps ticking underneath on its stale schedule and can fire on its old deadline anyway, silently discarding the acceleration. A fresh race: per iteration cancels the stale leg outright.
farm_plot.verse — one interact button, meaning decided by state
The scene-graph component, subclassing basic_interactable_component exactly like the custom-items module's item pickup. One interact path: plant on Empty, tend on Seeded/Sprout, harvest on Mature/Wilted — the plot's own blackboard decides which. On entering the scene, RunPlot races three watchers side by side: farm_growth.RunPlotFSM, the interact handler, and RunVisualSync — the set's ONE sampled watcher (every 0.5s), applying StageScale's uniform entity scale (0.15 Empty → 1.0 Mature, sagging to 0.55 on Wilted) via SetGlobalTransform whenever the state changed underneath it. Growth stages ARE entity state changes; the FSM itself never polls.
Planting is eight lines of failure-context discipline: HandlePlant hoists Table := farm_types.Registry() before the if: (allocation can't sit in a condition), then stacks the agent→player cast, the fallible Registry index, and the level gate in one condition list — Unlocked? converting the interface's logic answer into a decides check.
farm_progression.verse — the zero-storage reshape
Here's the headline in full. The first working version gave this module its own module-scope weak_map(player, farming_player_record). Then the island save-slot rule caught up with us — and our studio's shipped SaveSystem shows why it's right at scale: player_persistent_data.verse holds Bank/Inventory/Backpack/UnlockData/... — every system's persisted state as a FIELD — behind exactly one weak_map(player, player_persistent_data). minigame_data.verse's own doc comment states the rule plainly: "we can add more minigames here in the future without changing the player_data class." Add a field, never a map.
So the reshape: farm_persistent_data became a plain struct — not <persistable>, not weak-mapped — the exact SHAPE an integrating project embeds as one field of its own single player_data:
farm_persistent_data<public> := struct:
Level<public> : int = 1
Experience<public> : int = 0
Version<public> : int = 1
(Version stays so the field can evolve independently.) Every function is now a pure transform — record in, record out, this module reads and writes nothing:
AwardXP<public>(Record : farm_persistent_data, Amount : int) : farm_persistent_data =
NewXP := Record.Experience + Amount
farm_persistent_data{
Level := ComputeLevel(NewXP),
Experience := NewXP,
Version := Record.Version
}
ComputeLevel banks a compiler lesson: integer / in Verse is <decides> and returns rational, not int — the mined legacy code's (XP / XPPerLevel) + 1 would have failed on both counts at once. Never divide for a level; accumulate:
ComputeLevel(XP : int) : int =
var Level : int = 1
var Threshold : int = XPPerLevel
loop:
if (XP >= Threshold):
set Level += 1
set Threshold += XPPerLevel
else:
return Level
The gate itself is one ternary — a bare comparison is a decides expression, not a logic value:
IsCropUnlocked<public>(Record : farm_persistent_data, Crop : farm_types.crop_def)<transacts> : logic =
if (Record.Level >= Crop.RequiredLevel) then true else false
island_garden.verse — the demo that refuses to spend a save slot
The worked example: a creative_device that discovers placed plot meshes, attaches a farm_plot_entity_component to each, and cycles island_corn → sunset_berry across them with the Mod[Index, Rotation.Length] idiom. Its garden_yield_bridge implements BOTH interfaces — the set's one harvest path and one progression path:
GrantYield<override>(Player : player, Crop : farm_types.crop_def, Amount : int) : void =
Granter.GrantItem(Player)
HUD.Show(Player, HarvestedMsg(Crop.DisplayName, Amount))
AwardXP(Player, farm_progression.HarvestXPAward)
Native grant, HUD toast, XP award — one method, because a harvest becomes real in exactly one place. The demo's "storage" is one line:
var FarmRecords<private> : [player]farm_progression.farm_persistent_data = map{}
A plain class-instance field, NOT a weak_map — it doesn't survive the session, and that's the point. As the file's own comment puts it, this reusable module "must not spend any of an integrating island's persistent-storage budget" — even a just-for-the-demo persistent map would still burn one of the two slots. The bridge supplies storage, farm_progression supplies rules: its IsCropUnlocked/AwardXP overrides and its <transacts> GetRecord helper forward straight into the pure functions, reading and writing FarmRecords around them.
Walkthrough
Dropping it in. Place an island_garden device, link an item_granter_device and a hud_message_device, and scatter two or more plot meshes (any static mesh — the component only needs a mesh_component). OnBegin discovers the meshes and wires every plot's Sink and Progression to one shared bridge.
The growth loop. Interact with an Empty plot → it plants its assigned crop, if your farming level meets RequiredLevel. The plot visibly scales up through Seeded → Sprout → Mature as the FSM transitions. Interact while it grows to tend it — shaves TendAccelerateSeconds off the current timer. Interact while Mature to harvest: yield, toast, HarvestXPAward (200 XP). Leave it too long and it wilts — a wilted harvest still grants the reduced WiltedYieldAmount: not nothing, but a real penalty.
The progression_sink swap-in. On a real island, embed the struct as one field of your single player_data-style <persistable> class — FarmData : farm_progression.farm_persistent_data beside your Bank/Inventory fields — then swap the bridge's GetRecord/AwardXP/IsCropUnlocked bodies to read-modify-write that field instead of FarmRecords. That's the entire integration step — zero change to farm_progression, farm_types, or farm_growth. The harvest seam is symmetric: swap GrantYield's granter call for inv_core.For(Player).Add(YieldId, Amount) once this set and the custom inventory module combine into one compiled project.
Gotchas
This build leans on two compiler rules that recur across the series: the two-SpatialMath rule and the persistable weak_map rule (the island save budget). Its own lessons, confirmed on the real UEFN compiler:
Gate the set, not the files. Per-file compile runs fail on cross-file identifiers by design: compiled alone, farm_growth (22 errors), farm_plot (40), farm_progression (2) and island_garden (17) all fail per-file on Unknown identifier farm_types — Script error 3506, verdict NEEDS-SET; only farm_types.verse, which depends on nothing, passes standalone. The compile truth is the SET verdict: PASS, 0 errors, on the real UEFN compiler.
A bare logic variable is not an if condition (3513). if (Elapsed): break fails — the postfix ? in if (Elapsed?): converts a logic VALUE into the <decides> check an if actually wants.
A bare comparison can't be a return value (3512 + 3510, same line). A comparison is a decides-effect check, not a logic value — even in a function whose declared return type is logic. The idiom: if (Cond) then true else false.
Integer / is <decides> and returns rational (3512 + 3509, same token). There is no rational + int overload, so (XP / XPPerLevel) + 1 fails twice at once. Accumulate in a loop instead — ComputeLevel above.
Two SpatialMaths (3506). /Verse.org/SpatialMath's vector3 constructs from Forward/Left/Up, not X/Y/Z — the older /UnrealEngine.com/Temporary/SpatialMath is a different type. The plot's uniform stage scale:
<transacts> never propagates — decorate every hop. The effect system reads DECLARED decorations, not inferred purity: StageScale (a pure case over an enum) failed 3512 until decorated, and once progression_sink existed the requirement crossed three files — interface method → concrete override → GetRecord → farm_progression.IsCropUnlocked. The error surfaces at whichever hop is missing it.
A sibling file's top-level class needs no using — a file-named using fails (3506). using { farm_plot } fails: farm_plot.verse declares no module wrapper, so farm_plot_entity_component is referenced bare. using only brings a MODULE's members into scope — top-level type names across a compiled set are already visible.
Get the complete code — free
You've read the full walkthrough. The complete, copy-paste-ready Verse solution is free for members — sign in to unlock it.
Free with your BrainDead.TV / BrainDeadGuild Discord account. The walkthrough above is always free.
Check your understanding
Test yourself with an interactive quiz and track your progress + earn XP — free for members.
Turn this into a guided course
Add A reusable five-file plant-grow-tend-harvest simulation that closes the race-per-state arc (guard → companion → plant) — data-driven crops, a scene-graph plot component, a tend-accelerated growth timer, and a progression module that owns no storage at all: pure record-in/record-out transforms shaped to embed in an island's single player_data envelope to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.
Original guide generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.