-
Notifications
You must be signed in to change notification settings - Fork 220
New tool: Multihaul #1484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
New tool: Multihaul #1484
Changes from 139 commits
0ab4262
3657eba
2e675db
a508e66
75c597b
8da32eb
b827167
3e91375
4ee1a21
d912fc2
5a4b43a
7ae87b0
bcbcc3d
c623822
1d72e2a
dba3c1a
5894d49
66bc94e
a381a24
55777ed
0bc3aa2
f79f046
d5e8333
5839e89
5a1278e
9a12910
d7b8668
fc848c8
348aa13
1c8ab86
b541254
c80fd0a
27f92b8
e11daff
1271e42
aa7ec00
a827df4
64184e2
aa46986
2a78abc
dd2afe3
e187aed
c6cd65a
f39879f
bcd1d94
a811f4e
3c1113f
5f3bea4
a1754c3
18d804b
ce57986
93f002f
0828c9f
dd177e3
27e017f
9abc0cc
c61e2c1
125b9b1
969dc3b
bb30c35
120714f
4918c25
f661b82
dbcfc0a
e1d73ab
ef86f40
232ab56
c95408d
6d8f5e3
3ee0432
f5b19f3
112be4e
d44dcc3
996bec8
84ad664
989ae8d
2d23d4f
e4f035f
f1566b2
5d1699e
0b31ff1
c41e43a
fef261b
37c55c5
9ee5928
2572537
b84914f
81f3db2
cf05936
58b3688
cfb1e95
7134d53
986eeb5
f3e50c6
c2a4bd1
48cd99a
8cbf4b3
46d0f57
bfa99d6
20dd70d
640ca99
347d2a2
b187046
acaa671
a9b48d1
2e45d27
830de7a
e1e3c65
b96a5bf
a4845fd
e963afe
4b0f187
bb0d045
40cd433
64ac19e
d31b536
8ae5572
e94232c
3ccf8d0
80501dd
8f4c3e7
ea5a859
759332c
f2886ae
f21982f
d8eded8
ae59c6d
2b4f7b9
a76460f
bbead11
9410045
c799c3f
d910921
3f31da2
6049eae
a334119
b976bdf
35f4800
f30d4b2
84dcc48
43ab0bf
680ce70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| multihaul | ||
| ========= | ||
|
|
||
| .. dfhack-tool:: | ||
| :summary: Haulers gather multiple nearby items when using wheelbarrows. | ||
| :tags: fort productivity items stockpile | ||
|
|
||
| This tool allows dwarves to collect several adjacent items at once when | ||
| performing hauling jobs with a wheelbarrow. When enabled, new | ||
| ``StoreItemInStockpile`` jobs with wheelbarrows will automatically attach nearby items so | ||
| they can be hauled in a single trip. Jobs without wheelbarrows would try to attach one if autowheelbarrows option is on. | ||
| Items claimed by another jobs or already stored in stockpiles would be ignored. | ||
| By default, up to ten additional items within 10 tiles of the original item are collected. | ||
| Warning: Destination stockpile filters are currently ignored by the job (because of DF logic). Which items qualify can be controlled | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be highly desirable for the tool to respect the stockpile settings. Can you elaborate on what the issue is?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't found any way to check and use stockpile filters, so it is impossible to implement that, sadly |
||
| with the ``--mode`` option. | ||
| Basic usage of wheelbarrows remains the same: dwarfs would use them only if hauling item is heavier than 75 | ||
|
|
||
| Usage | ||
| ----- | ||
|
|
||
| :: | ||
|
|
||
| multihaul enable [<options>] | ||
| multihaul disable | ||
| multihaul status | ||
| multihaul config [<options>] | ||
| multihaul finish | ||
|
|
||
| The script can also be enabled persistently with ``enable multihaul``. | ||
|
|
||
| ``multihaul finish`` cancels any ``StoreItemInStockpile`` jobs that have lost | ||
| their wheelbarrows, freeing attached items. | ||
|
|
||
| Options | ||
| ------- | ||
|
|
||
| ``--radius <tiles>`` | ||
| Search this many tiles around the target item for additional items. Default | ||
| is ``10``. | ||
| ``--max-items <count>`` | ||
| Attach at most this many additional items to each hauling job. Default is | ||
| ``10``. | ||
| ``--mode <any|sametype|samesubtype|identical>`` | ||
| Control which nearby items are attached. ``any`` collects any items nearby, even if they are not related to an original job item, | ||
| ``sametype`` only matches the item type (like STONE or WOOD), ``samesubtype`` requires type and | ||
| subtype to match, and ``identical`` additionally matches material. The | ||
| default is ``sametype``. | ||
| ``--autowheelbarrows <on|off|enable|disable>`` | ||
| Automatically assign wheelbarrows to jobs that lack them. | ||
| ``--debug <on|off|enable|disable>`` | ||
| Show debug messages via ``dfhack.gui.showAnnouncement`` when items are | ||
| attached. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,329 @@ | ||
| -- Allow haulers to pick up multiple nearby items when using wheelbarrows | ||
| --@module = true | ||
| --@enable = true | ||
|
|
||
| local eventful = require('plugins.eventful') | ||
| local utils = require('utils') | ||
| local itemtools = reqscript('item') | ||
|
|
||
| local GLOBAL_KEY = 'multihaul' | ||
|
|
||
| local finish_jobs_without_wheelbarrow | ||
|
|
||
| local function get_default_state() | ||
| return { | ||
| enabled=false, | ||
| debug_enabled=false, | ||
| radius=10, | ||
| wheelbarrow_search_radius_k=5, | ||
| max_items=10, | ||
| mode='sametype', | ||
| autowheelbarrows=true | ||
| } | ||
| end | ||
|
|
||
| state = state or get_default_state() | ||
|
|
||
| function isEnabled() | ||
| return state.enabled | ||
| end | ||
|
|
||
| local function persist_state() | ||
| dfhack.persistent.saveSiteData(GLOBAL_KEY, state) | ||
| end | ||
|
|
||
| local function load_state() | ||
| state = get_default_state() | ||
| utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) | ||
| end | ||
|
|
||
| local function for_each_item_in_radius(x, y, z, radius, fn) | ||
| local xmin = math.max(0, x - radius) | ||
| local xmax = math.min(df.global.world.map.x_count - 1, x + radius) | ||
| local ymin = math.max(0, y - radius) | ||
| local ymax = math.min(df.global.world.map.y_count - 1, y + radius) | ||
| local bxmin, bxmax = math.floor(xmin/16), math.floor(xmax/16) | ||
| local bymin, bymax = math.floor(ymin/16), math.floor(ymax/16) | ||
| for by = bymin, bymax do | ||
| for bx = bxmin, bxmax do | ||
| local block = dfhack.maps.getTileBlock(bx*16, by*16, z) | ||
| if block then | ||
| for _, id in ipairs(block.items) do | ||
| local item = df.item.find(id) | ||
| if item and fn(item) then return end | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| local function get_job_stockpile(job) | ||
| local ref = dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER) | ||
| return ref and df.building.find(ref.building_id) or nil | ||
| end | ||
|
|
||
| local function items_identical(a, b) | ||
| return a:getType() == b:getType() and a:getSubtype() == b:getSubtype() and | ||
| a.mat_type == b.mat_type and a.mat_index == b.mat_index | ||
| end | ||
|
|
||
| local function items_sametype(a, b) | ||
| return a:getType() == b:getType() | ||
| end | ||
|
|
||
| local function items_samesubtype(a, b) | ||
| return a:getType() == b:getType() and a:getSubtype() == b:getSubtype() | ||
| end | ||
|
|
||
| local match_fns = { | ||
| any = function() return true end, | ||
| identical = items_identical, | ||
| sametype = items_sametype, | ||
| samesubtype = items_samesubtype, | ||
| } | ||
|
|
||
| local function items_match(a, b) | ||
| local fn = match_fns[state.mode] or match_fns.sametype | ||
| return fn(a, b) | ||
| end | ||
|
|
||
| local function emptyContainedItems(wheelbarrow) | ||
| local items = dfhack.items.getContainedItems(wheelbarrow) | ||
| if #items == 0 then return end | ||
|
|
||
| if state.debug_enabled then | ||
| dfhack.gui.showAnnouncement('multihaul: emptying wheelbarrow', COLOR_CYAN) | ||
| end | ||
|
|
||
| for _, item in ipairs(items) do | ||
| if item.flags.in_job then | ||
| local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) | ||
| if job_ref then | ||
| dfhack.job.removeJob(job_ref.data.job) | ||
| end | ||
| end | ||
| dfhack.items.moveToGround(item, wheelbarrow.pos) | ||
| end | ||
| finish_jobs_without_wheelbarrow() | ||
| end | ||
|
|
||
| local function add_nearby_items(job) | ||
| if #job.items == 0 then return end | ||
|
|
||
| local target = job.items[0].item | ||
| if not target then return end | ||
| local stockpile = get_job_stockpile(job) | ||
| if not stockpile then return end | ||
| local x,y,z = dfhack.items.getPosition(target) | ||
| if not x then return end | ||
|
|
||
| local cond = {} | ||
| itemtools.condition_stockpiled(cond) | ||
| local is_stockpiled = cond[1] | ||
|
|
||
| local function matches(it) | ||
| return items_match(it, target) | ||
| end | ||
|
|
||
| local count = 0 | ||
| for_each_item_in_radius(x, y, z, state.radius, function(it) | ||
| if it ~= target and not it.flags.in_job and it.flags.on_ground and | ||
| not it:isWheelbarrow() and not dfhack.items.isRouteVehicle(it) and | ||
| not is_stockpiled(it) and matches(it) then | ||
| dfhack.job.attachJobItem(job, it, df.job_role_type.Hauled, -1, -1) | ||
| count = count + 1 | ||
| if state.debug_enabled then | ||
| dfhack.gui.showAnnouncement( | ||
| ('multihaul: added %s to hauling job of %s'):format( | ||
| dfhack.items.getDescription(it, 0), dfhack.items.getDescription(target, 0)), | ||
| COLOR_CYAN) | ||
| end | ||
| if count >= state.max_items then return true end | ||
| end | ||
| end) | ||
| end | ||
|
|
||
| local function find_attached_wheelbarrow(job) | ||
| for _, jitem in ipairs(job.items) do | ||
| local item = jitem.item | ||
| if item and item:isWheelbarrow() then | ||
| if jitem.role ~= df.job_role_type.PushHaulVehicle then | ||
| return 'badrole' | ||
| end | ||
| local ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) | ||
| if ref and ref.data.job == job then | ||
| return item | ||
| end | ||
| end | ||
| end | ||
| return nil | ||
| end | ||
|
|
||
| local function find_free_wheelbarrow(stockpile) | ||
| if not df.building_stockpilest:is_instance(stockpile) then return nil end | ||
| local sx, sy, sz = stockpile.centerx, stockpile.centery, stockpile.z | ||
| local found | ||
| for_each_item_in_radius(sx, sy, sz, state.radius*state.wheelbarrow_search_radius_k or 10*state.wheelbarrow_search_radius_k, function(it) | ||
| if it:isWheelbarrow() and not it.flags.in_job then | ||
| found = it | ||
| return true | ||
| end | ||
| end) | ||
| return found | ||
| end | ||
|
|
||
|
|
||
| local function attach_free_wheelbarrow(job) | ||
| local stockpile = get_job_stockpile(job) | ||
| if not stockpile then return nil end | ||
| local wheelbarrow = find_free_wheelbarrow(stockpile) | ||
| if not wheelbarrow then return nil end | ||
| if dfhack.job.attachJobItem(job, wheelbarrow, | ||
| df.job_role_type.PushHaulVehicle, -1, -1) then | ||
| if state.debug_enabled then | ||
| dfhack.gui.showAnnouncement('multihaul: adding wheelbarrow to a job', COLOR_CYAN) | ||
| end | ||
| return wheelbarrow | ||
| end | ||
| end | ||
|
|
||
| function finish_jobs_without_wheelbarrow() | ||
| local count = 0 | ||
| for _, job in utils.listpairs(df.global.world.jobs.list) do | ||
| if job.job_type == df.job_type.StoreItemInStockpile and #job.items > 1 and not find_attached_wheelbarrow(job) then | ||
| for _, jobitem in ipairs(job.items) do | ||
| local item = jobitem.item | ||
| if item and item.flags.in_job then | ||
| local ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) | ||
| if ref and ref.data.job == job then | ||
| dfhack.job.removeJob(job) | ||
| end | ||
| end | ||
| end | ||
| job.items:resize(0) | ||
| job.completion_timer = 0 | ||
| count = count + 1 | ||
| end | ||
| end | ||
| if count > 0 then | ||
| dfhack.gui.showAnnouncement('multihaul: clearing stuck hauling job', COLOR_CYAN) | ||
| end | ||
| end | ||
|
|
||
| local function on_new_job(job) | ||
| if job.job_type ~= df.job_type.StoreItemInStockpile then return end | ||
| local wheelbarrow = find_attached_wheelbarrow(job) | ||
| if wheelbarrow == 'badrole' then return | ||
| end | ||
| if not wheelbarrow and state.autowheelbarrows then | ||
| wheelbarrow = attach_free_wheelbarrow(job) | ||
| end | ||
| if not wheelbarrow then return end | ||
| add_nearby_items(job) | ||
| emptyContainedItems(wheelbarrow) | ||
| end | ||
|
|
||
| local function enable(val) | ||
| state.enabled = val | ||
| if state.enabled then | ||
| eventful.onJobInitiated[GLOBAL_KEY] = on_new_job | ||
| else | ||
| eventful.onJobInitiated[GLOBAL_KEY] = nil | ||
| end | ||
| persist_state() | ||
| end | ||
|
|
||
| if dfhack.internal.IN_TEST then | ||
| unit_test_hooks = {on_new_job=on_new_job, enable=enable, | ||
| load_state=load_state} | ||
| end | ||
|
|
||
| -- state change handler | ||
|
|
||
| dfhack.onStateChange[GLOBAL_KEY] = function(sc) | ||
| if sc == SC_MAP_UNLOADED then | ||
| state.enabled = false | ||
| eventful.onJobInitiated[GLOBAL_KEY] = nil | ||
| return | ||
| end | ||
| if sc == SC_MAP_LOADED then | ||
| load_state() | ||
| end | ||
| end | ||
|
|
||
| if dfhack_flags.module then | ||
| return | ||
| end | ||
|
|
||
| local args = {...} | ||
| if dfhack_flags.enable then | ||
| if dfhack_flags.enable_state then | ||
| enable(true) | ||
| else | ||
| enable(false) | ||
| end | ||
| return | ||
| end | ||
|
|
||
| local function parse_options(start_idx) | ||
| local i = start_idx | ||
| while i <= #args do | ||
| local a = args[i] | ||
| if a == '--debug' then | ||
| local m = args[i + 1] | ||
| if m == 'off' or m == 'disable' then | ||
| state.debug_enabled = false | ||
| i = i + 1 | ||
| else | ||
| state.debug_enabled = true | ||
| end | ||
| elseif a == '--autowheelbarrows' then | ||
| local m = args[i + 1] | ||
| if m == 'on' or m == 'enable' then | ||
| state.autowheelbarrows = true | ||
| i = i + 1 | ||
| elseif m == 'off' or m == 'disable' then | ||
| state.autowheelbarrows = false | ||
| i = i + 1 | ||
| else | ||
| qerror('invalid autowheelbarrows option: ' .. tostring(m)) | ||
| end | ||
| elseif a == '--radius' then | ||
| i = i + 1 | ||
| state.radius = tonumber(args[i]) or state.radius | ||
| elseif a == '--max-items' then | ||
| i = i + 1 | ||
| state.max_items = tonumber(args[i]) or state.max_items | ||
| elseif a == '--mode' then | ||
| i = i + 1 | ||
| local m = args[i] | ||
| if m == 'any' or m == 'sametype' or m == 'samesubtype' or m == 'identical' then | ||
| state.mode = m | ||
| else | ||
| qerror('invalid mode: ' .. tostring(m)) | ||
| end | ||
| end | ||
| i = i + 1 | ||
| end | ||
| end | ||
|
|
||
| local cmd = args[1] | ||
| if cmd == 'enable' then | ||
| parse_options(2) | ||
| enable(true) | ||
| elseif cmd == 'disable' then | ||
| enable(false) | ||
| elseif cmd == 'status' or not cmd then | ||
| print((state.enabled and 'multihaul is enabled' or 'multihaul is disabled')) | ||
| print(('radius=%d max-items=%d mode=%s autowheelbarrows=%s debug=%s') | ||
| :format(state.radius, state.max_items, state.mode, state.autowheelbarrows and 'on' or 'off', state.debug_enabled and 'on' or 'off')) | ||
| elseif cmd == 'config' then | ||
| parse_options(2) | ||
| persist_state() | ||
| print(('multihaul config: radius=%d max-items=%d mode=%s autowheelbarrows=%s debug=%s') | ||
| :format(state.radius, state.max_items, state.mode, state.autowheelbarrows and 'on' or 'off', state.debug_enabled and 'on' or 'off')) | ||
| elseif cmd == 'unstuckjobs' then | ||
| finish_jobs_without_wheelbarrow() | ||
| else | ||
| qerror('Usage: multihaul [enable|disable|status|config|unstuckjobs] [--radius N] [--max-items N] [--mode MODE] [--autowheelbarrows on|off] [--debug on|off]') | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this actually changes gameplay mechanics rather than just automating existing mechanics, I think this should be
gameplayrather thanproductivity.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, would change it if you think it would fit it better