Roman and Carthaginian farms show presentation-only field workers. They are
the nation's own civilian humanoids -- the same rig, proportions, equipment
archetypes and palette the real civilian and builder units use, driven by the
baked clip bank through CreaturePipeline::submit_requests. They are not
entities: no population, selection, collision, pathfinding, vision, resource
output, or replay state is added.
Each nation registers its look once, from its own troop renderers, as a
NationCivilianRig (register_nation_civilian_rig): the civilian visual spec,
the civilian idle archetype for tending, the builder's sickle archetype for
reaping, and the civilian palette fill. Nothing about a farm worker's
appearance is authored in the farm code, so a change to a nation's civilians
carries straight over.
Registration then derives five archetypes by hanging the shared field props
(render/entity/farm_worker_props.cpp) on those two: a straw sun hat for
tending and reaping, hat plus a tied wheat sheaf for carrying, the sheaf alone
for a bare-headed carrier, and a hat tipped forward over the face for the
sleeper. A straw hat is field dress, not faction dress, so both nations share
it. Roughly one worker in five goes bare-headed, so a row of hats is not a
uniform. The props are registered as ordinary humanoid equipment
contributions, and head gear is authored against the rendered skull silhouette
(0.168), not the rig's nominal head radius.
A field carries one to three people, chosen by its entity hash
(farm_activity_roster): three a little over half the time, a pair a third
of the time -- as often a reaper with a gatherer as two reapers -- and
occasionally a lone worker. Fewer than three is deliberate; a street of farms
must not read as the same three people copied along it.
Workers 0 and 1 are lane workers. Each has a lane running between the crop
rows (farm_activity_lane): along x for the Roman field, along z for the
Punic one, offset a little from the row centre per worker. Their loop is three
bouts of work (a whole number of strokes: construct_chisel on growing
crops, construct_reap when ripe), a breather (idle, straightening up and
turning to look down the row) and a few steps along the lane (walk, phase
from distance covered, so feet do not skate). Over minutes a worker walks the
lane out and back; at the row end its heading swings round over the first
half-metre of the return rather than flipping. While working it stands turned
30-50 degrees into the standing crop; the turn in and out of that stance is
eased over the first part of the bout and the breather.
Worker 2 is the headland hand, on the strip outside the crop rectangle by
the gate. On a growing field it kneels at the edge row (construct_kneel_chisel)
with the same bout loop, shuffling along the headland. On a ripe field it runs
the harvest trip (farm_gatherer_beat): kneel and bundle at the row end,
stand and lift (the sheaf rig appears), carry it to the stook by the gate,
set it down (the plain rig returns), walk back. The walk beats last as long as
the actual distance takes at that worker's pace.
Every duration, stride, offset, stroke rate, lane offset, facing tilt and hat
is drawn from ambient_hash of the entity id with a per-worker sub-stream.
Nothing derives from animation_time alone. Two adjacent fields never share a
rhythm, and a hundred fields spread across work, breather and stepping at any
instant (FarmActivityTest.AHundredFieldsDoNotMoveInLockstep pins this on
both the pure schedule and the submitted bone palettes).
Clip changes are placed on shared poses -- work bouts end on a stroke
boundary, steps start from a standing idle -- and every pose also carries the
outgoing clip, its phase and a 0.35 s fade weight, which CivilianActor
passes to blend_out_interrupted_clip.
A bout's length comes from the tending rhythm whatever the crop's growth, and each bout divides it into a whole number of strokes near the reaping rhythm when the field is ripe. Ripening therefore changes how fast the sickle swings, never where the worker stands: letting the ripe rhythm set the bout lengths moved every lane worker along the row the moment a field ripened.
Walk speeds (0.62-0.80 m/s tending, 0.66-0.86 m/s carrying) keep the leg cadence near the baked walk's 1.09 Hz at the civilian scale, and idle plays at the baked 8 s breath -- faster values read as panting.
While a field stands ripe, cut sheaves are stood up in a stook beside the
gate on the headland (farm_activity_stook_anchor): five sheaves, one more
for each harvest the field has been through up to eight, in the field's own
straw tone. It is part of the field, not a person: it follows the field's own
fog and distance treatment and is still drawn when the actor budget is spent.
It disappears with the reset after harvest, when the grain has been carted
off. Nothing is stooked on a growing crop.
The crop generator keeps the two lane strips clear of stalks
(farm_activity_clearing), which reads as walked rows rather than bald
patches; the headland is outside the crop and needs no clearing. Actors are
people, so the field's own scale never reaches their world matrix; only its
placement and row orientation do.
Zero-growth plots, ruined building visuals, neutral farms, construction previews, pending removal, death and dismantling hide all actors. A builder assigned to the field -- harvesting it, raising it or repairing it -- suppresses the decorative workers there, because a real person is already standing in that field doing that job.
Eligibility is resolved for every field in two scans of the locked render snapshot at the top of the frame -- one over builder tasks, one over farms -- and submission is then a binary search on the resulting list. No component is looked up per field per frame, and the live simulation is never read; growth and harvest count come off the snapshot.
A farm carries a UnitComponent, so the render snapshot files it with the units
and it is drawn from Renderer::plan_unit_entry, not from
Renderer::submit_non_unit_entry. That is where the per-frame FarmActivity is
attached to the draw context; wiring it into the building path alone renders
nothing at all while every unit test stays green.
Each actor position resolves against the session terrain surface and the field's soil height. Frustum, current fog visibility (not merely explored terrain), lens occlusion, screen size and distance checks precede submission, per actor, at the actor's current place on its lane.
| Quality | Workers per field | Maximum submitted actors per world render |
|---|---|---|
| Low | 0 | 0 |
| Medium | 1 | 24 |
| High | 3 | 64 |
| Ultra | 3 | 96 |
The per-field cap is then the field's own roster. Workers below seven projected pixels or beyond 160 world units are culled. Below fifteen pixels, a field has at most one worker, always a lane worker. Earlier visible fields consume the budget; excess fields are skipped. The nap requires both lane workers visible and is disabled by the one-worker LOD and by pair rosters that field a gatherer instead.
Workers reuse the shared humanoid rigged-mesh and bone-palette caches, so they
cost the same per body as any other civilian on screen and warm through the
existing creature asset prewarm. Each body arrives as two rigged commands, the
body and its baked attachments. Below twelve projected pixels a worker drops
to CreatureLOD::Minimal; that tier resolves a per-state snapshot mesh instead
of the requested clip, so it is kept to sizes where the pose cannot be read.
The stook is one instanced archetype of at most 25 primitives. The per-frame
batch and the eligible-field and real-task lists retain their vector capacity
across frames.
The CPU sampling test reports dense_submission_p95_us for 100 field
submissions with the High cap, including field lookup, schedule sampling,
transforms, request assembly and a recording submitter. This is a CPU
microbenchmark, not a GPU frame-time measurement. Use the dense Arena scene on
target hardware to measure the full GPU cost; the actor/primitive caps are
structural budgets, not a promised millisecond cost.
One renderer-wide slot permits at most one sequence, and no extra actors are
spawned -- the two lane workers already on the field play it. The beats are
constants in Render::GL::Nap, in seconds from the moment the slot is claimed:
| Window | Sleeper (worker 0) | Coworker (worker 1) |
|---|---|---|
| 0.0-1.2 | sits down (showcase_rest_sit_down) |
works |
| 0.8-3.6 | -- | walks over (walk, phase from distance) |
| 1.2-4.0 | asleep, hat over the face (showcase_rest_sit) |
arrives, stands and stares |
| 4.0-4.7 | jolts up: the sit-down beat run backwards | still staring |
| 4.7-7.2 | works at more than twice the normal rate | still staring |
| 7.2-9.0 | back to its normal loop | walks back to wherever its loop now is |
Rest poses are showcase clips, so they sit on the ground without foot snapping.
The walk phase advances with distance covered rather than wall-clock, so the
coworker does not skate. The sequence only starts when the sleeper's own loop
keeps it on the spot for the full nine seconds (farm_worker_stays_put), so
nobody sleeps while sliding down the row. Worker 2 keeps working throughout.
SOI_FARM_GAG_SECONDS=<n> is the review hook: it forces the sequence to start
every n seconds on every eligible field, which is the only practical way to
capture something meant to happen twice an hour.
FarmActivity::cooldown_seconds defaults to 600 seconds and clamps to at least 300. Entity/cycle hashes select only roughly one quarter of eligible scheduling
windows, with different start offsets. Consecutive possible starts are separated
by at least the configured cooldown. Missed off-screen windows are not queued.
Loss of eligibility, visibility, budget or crop stage cancels the sequence;
renderer/world changes and time rewinds clear it. Simulation pause does not
advance it. No schedule data is persisted into gameplay state.
Build render_tests and arena_app. Run:
QT_QPA_PLATFORM=offscreen LC_ALL=C build/bin/render_tests --gtest_filter='FarmActivityTest.*' --gtest_output=xml:/tmp/farm-activity.xml
DISPLAY=:0 build/bin/arena_app --batch --scenario farm_activity_single --clean-capture --capture-interval 4 --artifact-dir /tmp/farm-single
DISPLAY=:0 build/bin/arena_app --batch --scenario farm_activity_dense --scenario-distance 0.55 --clean-capture --capture-interval 6 --artifact-dir /tmp/farm-dense
DISPLAY=:0 build/bin/arena_app --batch --scenario farm_activity_single --fog-of-war --clean-capture --capture-interval 4 --artifact-dir /tmp/farm-fog
SOI_FARM_GAG_SECONDS=10 DISPLAY=:0 build/bin/arena_app --batch --scenario farm_activity_lazy_farmer --duration 20 --scenario-distance 0.75 --clean-capture --capture-interval 1 --artifact-dir /tmp/farm-lazy
building_preview cannot show these workers: it replays captured primitive
draws on the CPU and has no rigged-mesh or bone-palette path, so the Arena is
the review hook. --scenario-distance is how you get close enough to read a
pose.
farm_activity_dense exercises 12 adjacent farms and the global cap over a
forty-second run -- long enough for the full growth/ripen/reset/destroy cycle
and short enough to stay inside the arena watchdog on software GL. Note that
its groups are seated with count = 2 and spacing = ±16 from x = ∓8, which
puts the first Roman and the first Punic field of each row on the same tile
(both at x = 0.5); the middle column on screen is therefore two farms drawn
on top of each other and shows their combined workers. Judge composition on
the outer columns. farm_activity_terrain places both factions on a hill.
The first three scenes start growing, ripen at eight seconds, reset growth at
sixteen and destroy the first pair at twenty-four, so one run covers active,
inactive and destroyed plots; normal growth resumes after the empty reset. Each
carries one Roman and one Carthaginian owner, which is what makes --fog-of-war
a real check: the other player's field and its workers must disappear together.
Graphics-quality overrides exercise the LOD tiers.
farm_activity_lazy_farmer is the exception. It frames a single field closely
and holds it ripe, because a growth change cancels the sequence by design and
would cut every capture short. Pair it with SOI_FARM_GAG_SECONDS; the
deterministic scheduling test already covers the real cooldown, the shared
concurrency cap and cancellation, so nobody has to sit through ten minutes of
wheat to check the timing.
A field that is ripe and waiting for a builder is marked with the same gold
illumination as a freshly recruited unit: a soft ground disc, two layered
healer_aura glows, a slow expanding ring and a handful of rising glints, all
breathing on a 2.4 s pulse (render_ripe_fields in
production_completion_renderer.cpp). It is deliberately not a LocalLight.
A torch-style light reads as natural firelight, and the point is to read as
"this field wants attention". Only the local player's own fields glow, and each
field has its own pulse phase, so a row of farms doesn't blink in unison.
Reduced motion freezes the pulse and drops the glints.
When a builder reaps the field, complete_food_harvest attaches the recruit
spawn flare to the farm itself (attach_harvest_flare, radius 0.42 of the
13.6 m footprint). The field flashes gold and rings outward exactly as a new
recruit does. While that flare plays, the ripe glow is suppressed. After it,
the field is green again.
The flare is ProductionCompletionComponent, and it must be copied into the
render snapshot (copy_presentation_snapshot_components). The effect pass
draws from the snapshot, and before that copy was added no recruit flare was
ever visible either.
When the builder delivers the sheaf, the "+N food" floating number rises from
the stone yard's drop point, not from the barracks entity. Anchoring on the
barracks let the per-anchor limit of two live numbers swallow most deliveries
in a busy economy (credit_load_at_yard in resource_delivery_system.cpp).
The same applies to every resource a hauler delivers.