Farming Module: One Pattern, Three Worlds — and Zero Storage of Its Own
Guideadvancedcompiles

Farming Module: One Pattern, Three Worlds — and Zero Storage of Its Own

Updated advancedModulesFarmingSimulationCode verified

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.

Comments

    Sign in to vote, comment, or suggest an edit.Sign in