multihaul: let citizens haul multiple items to a stockpile in one trip - #1636
Open
Alistair-Afton wants to merge 12 commits into
Open
Alistair-Afton wants to merge 12 commits into
Alistair-Afton wants to merge 12 commits into
Conversation
When a citizen picks up an item for a stockpile, they also grab up to "max" nearby loose items of the same type and drop everything off in one trip. Disabled by default. Extras are carried as Hauled inventory items; the job's own item refs are never touched (mutating job.items cancels the job). Since the game drops inventory items that are not linked to the job, extras are re-attached each poll while the job is in flight. On job end, extras are released onto the primary item's pile tile, the nearest pile tile, or the unit's feet if the job was cancelled far from the pile.
…b batching - 'targets all' also piggybacks loads into minecarts, barrels, and bins - 'types all' grabs items already claimed by other jobs bound for the same destination, letting one trip do several jobs' work - 'weight <n>' caps the total carried load in DF mass units - extras claimed by real jobs are never unflagged (JOB specific_ref check) - extras that fail container insertion fall back to the ground instead of stranding in inventory - stockpile anchors are picked by job ref role so wheelbarrow legs don't confuse the pickup item - item:getStockpile() is pcall-wrapped since it throws on item classes that lack the field - self-healing sweep clears orphaned in_job flags left by env reloads - in_job flags are cleared on map/world unload so saves never persist bogus claims
- 'weight auto' derives a per-unit carry cap from strength scaled by body size (a typical dwarf can manage ~900 units, about three boulders) - dwarves opportunistically grab additional eligible items they pass en route to the destination, not just at the pickup site - extras are now picked nearest-first instead of scan order - wheelbarrow-assisted haul jobs are skipped: the wheelbarrow already multi-hauls for the job - registered in the control panel gameplay group (off by default)
Adds a separate fetch radius (default 8) controlling how far around the pickup site a dwarf will go to collect extras, so they actively gather nearby goods rather than only taking what is directly adjacent. The existing 'radius' option (default 2) now only controls opportunistic pickups made along the route.
'types all' only extends to items already claimed by other jobs for the
same destination, since an unclaimed item of a different type might not
be allowed by the pile's filter. 'pile' lifts that restriction by
re-implementing item-vs-filter matching for the stockpile categories
whose settings have usable indices: stone, wood (via plant raws),
bars/blocks, gems, coins, weapons/trapcomps/ammo, all armor slots,
furniture, and finished goods.
The matcher checks every filter dimension DF exposes: category flags,
type/subtype vectors, matgloss-indexed metal/stone vectors, the
other_mats enums (coal/potash/ash/pearlash/soap, glass, wood/plant and
creature material classes via material id), core and improvement
quality, and links-only piles. Settings vectors surface as numbers and
populate lazily, so lookups normalize 0/1 to strict booleans and treat
empty vectors as the unconfigured all-allowed default. Anything that
cannot be proven acceptable - unmapped categories like food or refuse,
and ambiguous restrictions like usable/dyed/color - is rejected, so a
wrong answer only means fewer extras, never mis-stored items.
s_types_all (bool) migrates to s_types ('same'|'all'|'pile').
Extends the destination-filter matcher to the remaining stockpile categories so pile mode can piggyback far more item types: food categories via the organic material tables (including creature/caste indexed fish and eggs), leather, cloth and thread by material class, paper/parchment sheets, corpses and corpse pieces/remains by race and part kind (with fresh vs rotten hide handled separately), and cages or traps by occupant race or the empty-container toggles. Also fixes a mis-store where a rotten hide would be accepted by a pile allowing only fresh hide, adds REMAINS to the refuse branch, and adds a unit test suite exercising the matcher's discrimination on synthetic pile settings.
Tool items were the remaining common item type rejected outright. They now map onto furniture buckets through the itemdef's tool_use (TRACK_CART -> minecarts, HEAVY_OBJECT_HAULING -> wheelbarrows, FOOD_STORAGE -> large pots) or OTHER_LARGE_TOOLS via the FURNITURE flag, and non-furniture tools go through the finished-goods slot.
A stockpile's misc settings can forbid organic or inorganic items pile-wide (a real fort had a pile with organic disabled). Only mat_type 0 counts as provably inorganic; organic, builtin, and unknown materials are all gated by allow_organic.
- never grab the destination container itself: a vehicle waiting on the ground for its StoreItemInVehicle job matched claimed_for_dest against its own job and could be pocketed as an extra - do not steal items claimed by wheelbarrow-assisted haul jobs: they already multi-haul, and the grab would orphan their job's goods - credit extra weight back when an extra drops out mid-flight so the carry cap is not inflated by items no longer riding along - scan the map blocks overlapping the search square instead of all of IN_PLAY when looking for candidates (25k+ items in a mature fort) - keep the repeat timer alive across map unload and transient getCitizens failures, and validate persisted settings on load - reject garbage numeric args on the CLI instead of silently keeping the old value
A unit that dies or goes off-map is never scanned again, so its tracked entry and extras would linger until unload. Prune tracked entries for units absent from getCitizens each pass so their extras are released through the normal path (dropped at the corpse or destination). Also add unit tests covering extra_ok's type-mode discrimination, weight cap boundary, destination-container exclusion, and claimed_for_dest's pile matching including the wheelbarrow exclusion.
Alistair-Afton
force-pushed
the
multihaul
branch
from
September 19, 2026 23:21
c3a0e65 to
db3aede
Compare
Member
|
we need to discuss whether this should be classified as an armok tool or if instead needs a new category for providing a material change in game mechanics that goes beyond mere automation |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New opt-in script
multihaul(disabled by default; also registered in the control panel's gameplay section). When a citizen picks up an item for a stockpile, they also grab up tomaxadditional loose items of the same type — detouring around the pickup site to collect them (fetchradius) and opportunistically picking up more along the way (radius) — then drop everything off in one trip.Implementation notes
The job's own
job.itemsvector is never touched — addingjob_item_refs to a live job reliably cancels it (verified in-game). Instead, extras ride asHauledinventory items flaggedin_jobso other jobs don't claim them. Since the engine drops inventory items that aren't linked to the active job, a 3-frame poll re-attaches extras while the job is in flight. Wheelbarrow-assisted jobs are skipped since the wheelbarrow already multi-hauls.Extras nearest to the origin are preferred. The pickup-site sweep uses
fetch; en-route grabs (throttled to every few polls) useradiusaround the unit's current position.On job end, extras are released onto the primary item's landing tile (if it landed on the destination pile), else the nearest pile tile when the unit finishes at/next to the pile (e.g. vehicle handoffs), else the unit's feet for mid-route cancellations. For container destinations, extras the unit still holds (or that were dropped at its feet during the handoff) are inserted via
dfhack.items.moveToContainer, falling back to a ground drop if insertion fails (e.g. wrong material class). Extras dropped directly onto the destination pile or into the container mid-transit are released as delivered.types allonly ever takes items that provably share the job's destination: a claimed item's ownJOBspecific_ref must resolve to the same pile or container. Unrelated jobs are never touched, and an extra that gets legitimately re-claimed by a real job (flag vs. specific_ref check) is left alone rather than unflagged.types pilere-implements DF's item-vs-stockpile-filter match in Lua (no such check is exposed). It covers every category with usable indices: stone, wood (via plant raws), bars/blocks (incl. coal/potash/ash/glass/soapother_matsmapping), gems, coins, weapons/trapcomps/ammo, armor, furniture (incl. tool buckets via the itemdef'stool_use), finished goods, food organics viamat_table.organic_types/indexes(incl. creature/caste-indexed fish and eggs), leather, cloth/thread by material class, paper/parchment sheets, corpses and corpse pieces/remains by race and part kind (fresh vs. rotten hide separately), and cages/traps by occupant race or the empty-container toggles. Quality (quality_core/quality_total, incl. improvements), dyed/undyed, dye color, usable/unusable, and links-only restrictions are honored; stockpile settings that were never configured (empty vectors, all-false arrays) correctly mean "all allowed". Anything that cannot be proven acceptable is rejected, so a wrong answer can only ever mean fewer extras, never a mis-stored item.Candidate filter excludes items that are forbidden, owned, hostile, trader, rotten, dump/melt-designated, in containers, in buildings, spider webs, construction-designated, hidden, burning, storage-assigned (
getStockpile(), pcall-wrapped since the field doesn't exist on all item classes), or already sitting on a building tile (so items are never pulled out of neighboring stockpiles).Cleanup paths:
disablereleases all in-flight extras; a periodic sweep clears anyin_jobflag that has noJOBspecific_ref behind it (heals leaks from script/env reloads); flags are also cleared on map/world unload so saves never persist bogus claims.Additional audit fixes: the destination container itself can never be pocketed as an extra (a vehicle waiting on the ground for its own
StoreItemInVehiclejob matched the claim check against itself); items claimed by wheelbarrow-assisted haul jobs are not stolen (their jobs do not resolve throughget_job_dest); dropped extras credit their weight back so the cap is not inflated; candidate scanning reads the overlapping map blocks' item lists instead of all ofIN_PLAY(~26k items in the test fort); the repeat timer survives map unload and transient scan errors; tracked units that leave the citizen list (dead/off-map) release their extras immediately.Test plan
Verified live in a mature fort (DF 53.16):
Hauleditems tolerated in one inventoryfetchcollects extras 4-5 tiles beyond the pickup point and delivers them to the destination piledisablewith in-flight extras: all released cleanly, no orphanedin_jobitems (re-claimed stragglers verified viaspecific_refs)job.itemsmutation confirmed unsafe (job cancel + orphan) — avoided entirelymoveToContainerverified; primary items merging into container stacks detected as deliveredweight auto: per-unit caps verified (e.g. 703 for a weak hauler vs 1206 for a strong one; loads stop at the cap mid-pickup)dfhack.printerr)types pile: cross-type extras attach at pickup and en route and are delivered into the destination pile (observed live: thread riding a corpse haul into a mixed refuse/leather/cloth pile)claimed_for_destreturns false for a wheelbarrow-claimed boulder, true for a normal same-pile claimtypes pilediscrimination on real piles: wood-only pile takes wood/rejects stone, bars piles distinguish metals vs. potash-only vs. soap-only, links-only piles reject everything, forbidden/owned/in-building candidates rejectedin_jobflags aftertest/multihaul.lua: 23 unit tests / 67 checks over the matcher (subtype, material, quality, dye/color, usability, freshness, race vectors, tool buckets, unconfigured-pile semantics) and the candidate filtersextra_ok/claimed_for_dest(type modes, weight-cap boundary, destination-container exclusion, wheelbarrow-claim exclusion)