From 5e767343cc1c7dfe71f7155f9f36995a15cc253a Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 19 Sep 2025 18:47:08 +0800 Subject: [PATCH 1/2] Add new tool fix/archery-practice and its doc --- docs/fix/archery-practice.rst | 58 ++++++++ fix/archery-practice.lua | 205 ++++++++++++++++++++++++++++ internal/control-panel/registry.lua | 3 + 3 files changed, 266 insertions(+) create mode 100644 docs/fix/archery-practice.rst create mode 100644 fix/archery-practice.lua diff --git a/docs/fix/archery-practice.rst b/docs/fix/archery-practice.rst new file mode 100644 index 000000000..3916ce56a --- /dev/null +++ b/docs/fix/archery-practice.rst @@ -0,0 +1,58 @@ +fix/archery-practice +==================== + +.. dfhack-tool:: + :summary: Consolidate and remove extra ammo items to fix 'Soldier (no item)' issue. + :tags: fort bugfix items + +Combine ammo items inside quivers that are assigned for training to allow +archery practice to take place. + +Usage +----- + +``fix/archery-practice`` + Combine ammo items inside quivers that are assigned for training. + +``fix/archery-practice -q``, ``fix/archery-practice --quiet`` + Combine ammo items inside quivers that are assigned for training. + Do not print to console. + +This tool will only combine ammo items inside the quivers of units in +a squad that is currently set to train. + +The 'Soldier (no item)' issue +----------------------------- + +Due to a bug in the game, a unit that is scheduled to train will not be +able to practice archery at the archery range when their quiver contains +more than one stack of ammo item that is assigned to them for training. +This is indicated on the unit by the 'Soldier (no item)' status. + +The issue occurs when the game assigns an ammo item with a stack sizes of +less than 25 to the unit, prompting the game to assign additional stacks +of ammo items to make up for the deficit. + +The workaround to this issue is to ensure the squad ammo assignments for +use in training are filled with ammo items with stack sizes of at least 25. +Since training bolts that are often made of wood or bone are created in +stacks of 5, the use of ``combine`` on ammo stockpiles is recommended to +reduce the frequency of this issue occurring, while "incomplete" stacks of +ammo items that are already inside the quivers of training units can be +managed by this tool. + +Any other stacks of ammo items inside the quiver that are not assigned +for training will not affect the unit's ability to practice archery. + +Limitations +----------- + +Due to the very limited number of ammo items a unit's quiver might contain, +the material, quality and maker of the items are ignored when combining them. +Only ammo items assigned for training will be combined, while ammo items +inside the quiver that are assigned for combat will not be affected. + +Although this tool will consolidate ammo items inside quivers and discard +any surplus items, the training units may not immediately go for archery +practice, especially if they are still trying to collect more ammo items +that the game have assigned to them. diff --git a/fix/archery-practice.lua b/fix/archery-practice.lua new file mode 100644 index 000000000..0f79dda6a --- /dev/null +++ b/fix/archery-practice.lua @@ -0,0 +1,205 @@ +-- Consolidate and remove extra ammo items to fix 'Soldier (no item)' issue. + +local argparse = require("argparse") +local utils = require('utils') + +local function GetTrainingSquads() + local trainingSquads = {} + for _, squad in ipairs(df.global.world.squads.all) do + if squad.entity_id == df.global.plotinfo.group_id then + if #squad.ammo.ammunition > 0 and squad.activity ~= -1 then + trainingSquads[#trainingSquads + 1] = squad + end + end + end + return trainingSquads +end + +local function isTrainingAmmo(ammoItem, squad) + for _, ammoSpec in ipairs(squad.ammo.ammunition) do + if ammoSpec.flags.use_training then + for _, id in ipairs(ammoSpec.assigned) do + if ammoItem.id == id then return true end + end + end + end + return false +end + +local function GetTrainingAmmo(quiver, squad) + local trainingAmmo = {} + for _, generalRef in ipairs(quiver.general_refs) do + if df.general_ref_contains_itemst:is_instance(generalRef) then + local containedAmmo = generalRef + local ammoItem = containedAmmo and df.item.find(containedAmmo.item_id) + if isTrainingAmmo(ammoItem, squad) then + trainingAmmo[#trainingAmmo + 1] = ammoItem + end + end + end + return trainingAmmo +end + +local function UnassignAmmo(trainingAmmo, itemToKeep, itemsToRemove, squad, unit) + local plotEqAssignedAmmo = df.global.plotinfo.equipment.items_assigned.AMMO + local plotEqUnassignedAmmo = df.global.plotinfo.equipment.items_unassigned.AMMO + local uniforms = { + unit.uniform.uniforms.CLOTHING, + unit.uniform.uniforms.REGULAR, + unit.uniform.uniforms.TRAINING, + unit.uniform.uniforms.TRAINING_RANGED + } + for _, ammoItem in ipairs(trainingAmmo) do + if ammoItem ~= itemToKeep then + local idx + local assignedAmmo + for _, ammoSpec in ipairs(squad.ammo.ammunition) do + if ammoSpec.flags.use_training then + idx = utils.linear_index(ammoSpec.assigned, ammoItem.id) + if idx then + assignedAmmo = ammoSpec.assigned + goto unassignAmmo + end + end + end + ::unassignAmmo:: + if assignedAmmo and idx then + -- Unassign ammo item from squad. + assignedAmmo:erase(idx) + idx = utils.linear_index(squad.ammo.ammo_items, ammoItem.id) + if idx then + -- Remove item/unit pairings. + squad.ammo.ammo_items:erase(idx) + squad.ammo.ammo_units:erase(idx) + end + idx = utils.linear_index(plotEqAssignedAmmo, ammoItem.id) + if idx then + -- Move ammo item from assigned ammo list to unassigned ammo list. + plotEqAssignedAmmo:erase(idx) + plotEqUnassignedAmmo:insert('#', ammoItem.id) + utils.sort_vector(plotEqUnassignedAmmo) + end + end + for _, uniform in ipairs(uniforms) do + -- Remove ammo item from uniform. + idx = utils.linear_index(uniform, ammoItem.id) + if idx then uniform:erase(idx) end + end + if not utils.linear_index(itemsToRemove, ammoItem) then + -- Force drop ammo item to avoid issue recurring if game reassigns the ammo item to squad. + -- unit.uniform.uniform_drop:insert('#', ammoItem.id) + -- Units that choose to haul the surplus ammo items to stockpiles instead of just dropping them + -- on the ground will cancel their archery practice and put away the ammo item they were supposed + -- to train with as well. Force dropping the surplus item with moveToGround circumvents this. + local pos = unit and xyz2pos(dfhack.units.getPosition(unit)) + dfhack.items.moveToGround(ammoItem, pos) + end + end + end + -- Prompt unit to drop item. + -- unit.uniform.pickup_flags.update = true +end + +-- For practicality, item material, quality, and its creator (for masterworks), is ignored +-- for the purpose of combining the limited number of ammo items inside a quiver. +local function ConsolidateAmmo(trainingAmmo, squad, unit) + local itemToKeep + local itemsToRemove = {} + -- Check first if any training ammo item already has a stack size of 25 or higher. + for _, ammoItem in ipairs(trainingAmmo) do + if ammoItem.stack_size >= 25 then + itemToKeep = ammoItem + goto unassignAmmo + end + end + for _, ammoItem in ipairs(trainingAmmo) do + if not itemToKeep then + -- Keep the first item. + itemToKeep = ammoItem + goto nextItem + end + if itemToKeep and ammoItem ~= itemToKeep and itemToKeep.stack_size < 25 then + local combineSize = 25 - itemToKeep.stack_size + if ammoItem.stack_size > combineSize then + itemToKeep.stack_size = itemToKeep.stack_size + combineSize + ammoItem.stack_size = ammoItem.stack_size - combineSize + else + itemToKeep.stack_size = itemToKeep.stack_size + ammoItem.stack_size + itemsToRemove[#itemsToRemove + 1] = ammoItem + end + end + ::nextItem:: + end + ::unassignAmmo:: + -- Unassign surplus ammo items first before removing any from the game. + UnassignAmmo(trainingAmmo, itemToKeep, itemsToRemove, squad, unit) + if #itemsToRemove > 0 then + for _, item in ipairs(itemsToRemove) do + dfhack.items.remove(item) + end + end +end + +local function FixTrainingUnits(trainingSquads, options) + local totalTrainingAmmo = 0 + local consolidateCount = 0 + for _, squad in ipairs(trainingSquads) do + for _, position in ipairs(squad.positions) do + if position.occupant == -1 then goto nextPosition end + local unit = df.unit.find(df.historical_figure.find(position.occupant).unit_id) + local quiver = unit and df.item.find(position.equipment.quiver) + if quiver then + local trainingAmmo = GetTrainingAmmo(quiver, squad) + if #trainingAmmo > 1 then + if not options.quiet then + local unitName = unit and dfhack.units.getReadableName(unit) + print(('Consolidating training ammo for %s...'):format(unitName)) + end + totalTrainingAmmo = totalTrainingAmmo + #trainingAmmo + ConsolidateAmmo(trainingAmmo, squad, unit) + consolidateCount = consolidateCount + 1 + end + end + ::nextPosition:: + end + end + if not options.quiet then + if consolidateCount > 0 then + print(('%d stacks of ammo items in %d quiver(s) consolidated.'):format(totalTrainingAmmo, consolidateCount)) + else + print('No stacks of ammo items require consolidation.') + end + end +end + +local function ParseCommandLine(args) + local options = { + help = false, + quiet = false + } + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler = function() options.help = true end}, + {'q', 'quiet', handler=function() options.quiet = true end} + }) + return options +end + +local function Main(args) + local options = ParseCommandLine(args) + if args[1] == 'help' or options.help then + print(dfhack.script_help()) + return + end + local trainingSquads = GetTrainingSquads() + if #trainingSquads < 1 then + if not options.quiet then print('No ranged squads are currently training.') end + return + end + FixTrainingUnits(trainingSquads, options) +end + +if not dfhack.isSiteLoaded() and not dfhack.world.isFortressMode() then + qerror('This script requires the game to be in fortress mode.') +end + +Main({...}) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 08b721f8b..95f941b44 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -74,6 +74,9 @@ COMMANDS_BY_IDX = { -- can be restored here once we solve issue #4292 -- {command='craft-age-wear', help_command='tweak', group='bugfix', mode='tweak', default=true, -- desc='Allows items crafted from organic materials to wear out over time.'}, + {command='fix/archery-practice', group='bugfix', mode='repeat', default=true, + desc='Consolidate ammo items inside quivers to allow archery practice to take place.', + params={'--time', '449', '--timeUnits', 'ticks', '--command', '[', 'fix/archery-practice', '-q', ']'}}, {command='fix/blood-del', group='bugfix', mode='run', default=true}, {command='fix/dead-units', group='bugfix', mode='repeat', default=true, desc='Fix units still being assigned to burrows after death.', From 6c966cb120283129ae3645c485e3c8646709a4cf Mon Sep 17 00:00:00 2001 From: git--amade Date: Sat, 20 Sep 2025 00:00:58 +0800 Subject: [PATCH 2/2] Revise documentation, add changelog entry --- changelog.txt | 1 + docs/fix/archery-practice.rst | 30 +++++++++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5ecf8024e..67ca34a5b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `fix/archery-practice`: combine ammo items in units' quivers to fix 'Soldier (no item)' issue ## New Features diff --git a/docs/fix/archery-practice.rst b/docs/fix/archery-practice.rst index 3916ce56a..8054dc5de 100644 --- a/docs/fix/archery-practice.rst +++ b/docs/fix/archery-practice.rst @@ -18,8 +18,11 @@ Usage Combine ammo items inside quivers that are assigned for training. Do not print to console. -This tool will only combine ammo items inside the quivers of units in -a squad that is currently set to train. +This tool will combine ammo items inside the quivers of units in squads +that are currently set to train with the objective of ensuring that each +unit hold only one combined stack of ammo item assigned for training in +their quiver. Any ammo items left over after the combining operation +will be dropped on the ground. The 'Soldier (no item)' issue ----------------------------- @@ -29,17 +32,17 @@ able to practice archery at the archery range when their quiver contains more than one stack of ammo item that is assigned to them for training. This is indicated on the unit by the 'Soldier (no item)' status. -The issue occurs when the game assigns an ammo item with a stack sizes of +The issue occurs when the game assigns an ammo item with a stack size of less than 25 to the unit, prompting the game to assign additional stacks of ammo items to make up for the deficit. -The workaround to this issue is to ensure the squad ammo assignments for -use in training are filled with ammo items with stack sizes of at least 25. -Since training bolts that are often made of wood or bone are created in -stacks of 5, the use of ``combine`` on ammo stockpiles is recommended to -reduce the frequency of this issue occurring, while "incomplete" stacks of -ammo items that are already inside the quivers of training units can be -managed by this tool. +The workaround to this issue is to ensure the squad ammo assignments +for use in training contain as few ammo items with stack sizes smaller +than 25 as possible. Since training bolts are often made from wood or +bone which are created in stacks of 5, the use of the ``combine`` tool on +ammo stockpiles is recommended to reduce the frequency of this issue +occurring, while "incomplete" stacks of ammo items that are already +picked up by training units can be managed by this tool. Any other stacks of ammo items inside the quiver that are not assigned for training will not affect the unit's ability to practice archery. @@ -48,9 +51,10 @@ Limitations ----------- Due to the very limited number of ammo items a unit's quiver might contain, -the material, quality and maker of the items are ignored when combining them. -Only ammo items assigned for training will be combined, while ammo items -inside the quiver that are assigned for combat will not be affected. +the material, quality, and maker of the items are ignored when performing +the combining operation on them. Only ammo items assigned for training will +be combined, while ammo items inside the quiver that are assigned for combat +will not be affected. Although this tool will consolidate ammo items inside quivers and discard any surplus items, the training units may not immediately go for archery