From 519e92d53bc8eb4ccd2bdf82d3a0839281c3a785 Mon Sep 17 00:00:00 2001 From: Omar El-Koussy Date: Sun, 14 Jun 2026 22:17:15 -0400 Subject: [PATCH 1/5] Add locate-stone tool and docs Linked with issue 2679. Introduce a new DFHack script (locate-stone.lua) and documentation (docs/locate-stone.rst). The tool is a fork of locate-ore that also detects stone and fuel sources by scanning mineral events. Flux stones are not mineral events and so was not able to add them. First draft so would appreciate any feedback. --- docs/locate-stone.rst | 64 ++++++++++++ locate-stone.lua | 233 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 docs/locate-stone.rst create mode 100644 locate-stone.lua diff --git a/docs/locate-stone.rst b/docs/locate-stone.rst new file mode 100644 index 000000000..7bc5b3135 --- /dev/null +++ b/docs/locate-stone.rst @@ -0,0 +1,64 @@ +locate-stone +============ + +.. dfhack-tool:: + :summary: Finds stone and ore mineral events on the map. + :tags: fort armok inspection productivity + +Fork of `locate-ore` to also include stone and fuel sources. +This tool scans the map for stone and ore mineral events. With no arguments, or with +``list``, prints a list of stone and ore types with tile +counts. + +With a stone or ore or metal argument, selects a random matching undesignated wall tile, +centers the camera on it, and designates it for digging. + +If you want to dig **all** tiles of that kind of ore, highlight that tile with the +keyboard cursor and run `digtype `. + +By default, the tool only searches ore veins that your dwarves have discovered. + +Note that looking for a particular metal might find an ore that contains that +metal along with other metals. For example, locating silver may find +tetrahedrite, which contains silver and copper. + +``locate-stone`` only scans mineral events, so it is quick but does not find +ordinary layer-stone walls like flux stones. + +Usage +----- + +``locate-stone`` + List discovered visible stone and ore mineral events. + +``locate-stone [] `` + Select a random matching undesignated wall tile and designate it for digging. + +Examples +-------- + +``locate-stone -all`` + List all mineral events on the map, including those that have not been discovered. + +``locate-stone iron`` + Find and designate a visible tile of any ore that produces iron. + +``locate-stone magnetite`` + Find and designate a visible magnetite tile. + +``locate-stone coal`` + Find and designate a visible bituminous coal or lignite tile. + +``locate-stone -a lignite`` + Include undiscovered/unrevealed mineral events when searching for lignite. + +Options +------- + +``-a``, ``--all`` + Include undiscovered/unrevealed mineral events. + +Aliases +------- + +``coal``, ``coke``, and ``fuel`` match both ``coal_bituminous`` and ``lignite``. diff --git a/locate-stone.lua b/locate-stone.lua new file mode 100644 index 000000000..5582ded83 --- /dev/null +++ b/locate-stone.lua @@ -0,0 +1,233 @@ +-- Scan the map for stone and ore veins + +local argparse = require('argparse') + +local function extractKeys(target_table) + local keyset = {} + for k, _ in pairs(target_table) do + table.insert(keyset, k) + end + return keyset +end + +local function getRandomTableKey(target_table) + if not target_table then + return nil + end + + local keyset = extractKeys(target_table) + if #keyset == 0 then + return nil + end + + return keyset[math.random(#keyset)] +end + +local function getRandomFromTable(target_table) + if not target_table then + return nil + end + + local key = getRandomTableKey(target_table) + if not key then + return nil + end + + return target_table[key] +end + +local function sortTableBy(tbl, sort_func) + local sorted = {} + for _, value in pairs(tbl) do + table.insert(sorted, value) + end + + table.sort(sorted, sort_func) + + return sorted +end + +local function matchesMetalOreById(mat_indices, target_ore) + for _, mat_index in ipairs(mat_indices) do + local metal_raw = df.global.world.raws.inorganics.all[mat_index] + if metal_raw ~= nil and string.lower(metal_raw.id) == target_ore then + return true + end + end + + return false +end + +local product_by_inorganic_id = { + coal_bituminous = {'coke'}, + lignite = {'coke'}, +} + +local alias_targets = { + coal = { + coal_bituminous = true, + lignite = true, + }, + coke = { + coal_bituminous = true, + lignite = true, + }, + fuel = { + coal_bituminous = true, + lignite = true, + }, +} + +local function matchesStoneAlias(raw_id, target_stone) + local targets = alias_targets[target_stone] + return targets ~= nil and targets[raw_id] == true +end + +local function hasFlag(flags, flag) + if not flags then + return false + end + + local ok, value = pcall(function() return flags[flag] end) + return ok and value +end + +local function isStoneOrOre(ino_raw) + return hasFlag(ino_raw.flags, 'METAL_ORE') or + hasFlag(ino_raw.material.flags, 'IS_STONE') +end + +local tile_attrs = df.tiletype.attrs + +local function isValidMineralTile(opts, pos, check_designation) + if not opts.all and not dfhack.maps.isTileVisible(pos) then return false end + local tt = dfhack.maps.getTileType(pos) + if not tt then return false end + return tile_attrs[tt].material == df.tiletype_material.MINERAL and + (not check_designation or dfhack.maps.getTileFlags(pos).dig == df.tile_dig_designation.No) and + tile_attrs[tt].shape == df.tiletype_shape.WALL +end + +local function findStones(opts, check_designation, target_stone) + local stone_types = {} + for _, block in ipairs(df.global.world.map.map_blocks) do + for _, bevent in ipairs(block.block_events) do + if bevent:getType() ~= df.block_square_event_type.mineral then + goto skipevent + end + + local ino_raw = df.global.world.raws.inorganics.all[bevent.inorganic_mat] + if not isStoneOrOre(ino_raw) then + goto skipevent + end + + if not opts.all and not bevent.flags.discovered then + goto skipevent + end + + local lower_raw = string.lower(ino_raw.id) + local matches_target = not target_stone or lower_raw == target_stone or matchesStoneAlias(lower_raw, target_stone) + if not matches_target and hasFlag(ino_raw.flags, 'METAL_ORE') and ino_raw.metal_ore then + matches_target = matchesMetalOreById(ino_raw.metal_ore.mat_index, target_stone) + end + + if matches_target then + local positions = ensure_key(stone_types, bevent.inorganic_mat, { + inorganic_id = ino_raw.id, + inorganic_mat = bevent.inorganic_mat, + metal_ore = ino_raw.metal_ore, + positions = {} + }).positions + local block_pos = block.map_pos + for y=0,15 do + local row = bevent.tile_bitmask.bits[y] + for x=0,15 do + if row & (1 << x) ~= 0 then + local pos = xyz2pos(block_pos.x + x, block_pos.y + y, block_pos.z) + if isValidMineralTile(opts, pos, check_designation) then + table.insert(positions, pos) + end + end + end + end + end + :: skipevent :: + end + end + + -- trim veins with zero valid tiles + for key,vein in pairs(stone_types) do + if #vein.positions == 0 then + stone_types[key] = nil + end + end + + return stone_types +end + +local function designateDig(pos) + local designation = dfhack.maps.getTileFlags(pos) + designation.dig = df.tile_dig_designation.Default + dfhack.maps.getTileBlock(pos).flags.designated = true +end + +local function getStoneDescription(opts, vein) + local visible = opts.all and '' or 'visible ' + local str = ('%5d %stile(s) of %s'):format(#vein.positions, visible, tostring(vein.inorganic_id):lower()) + if vein.metal_ore and #vein.metal_ore.mat_index > 0 then + str = str .. ' (' + for _, mat_index in ipairs(vein.metal_ore.mat_index) do + local metal_raw = df.global.world.raws.inorganics.all[mat_index] + str = ('%s%s, '):format(str, string.lower(metal_raw.id)) + end + str = str:gsub(', %s*$', '') .. ')' + elseif product_by_inorganic_id[string.lower(tostring(vein.inorganic_id))] then + str = str .. ' (' + for _, product_id in ipairs(product_by_inorganic_id[string.lower(tostring(vein.inorganic_id))]) do + str = ('%s%s, '):format(str, product_id) + end + str = str:gsub(', %s*$', '') .. ')' + end + + return str +end + +local function selectStoneTile(opts, target_stone) + local stone_types = findStones(opts, true, target_stone) + local target_vein = getRandomFromTable(stone_types) + if target_vein == nil then + local visible = opts.all and '' or 'visible ' + qerror('Cannot find any undesignated ' .. visible .. target_stone) + end + local target_pos = target_vein.positions[math.random(#target_vein.positions)] + dfhack.gui.revealInDwarfmodeMap(target_pos, true, true) + designateDig(target_pos) + print(('Here is some %s'):format(target_vein.inorganic_id)) +end + +local opts = { + all=false, + help=false, +} + +local positionals = argparse.processArgsGetopt({...}, { + {'a', 'all', handler=function() opts.all = true end}, + {'h', 'help', handler=function() opts.help = true end}, +}) + +local target_stone = positionals[1] +if target_stone == 'help' or opts.help then + print(dfhack.script_help()) + return +end + +if not target_stone or target_stone == 'list' then + local stone_types = findStones(opts, false) + local sorted = sortTableBy(stone_types, function(a, b) return #a.positions < #b.positions end) + + for _,stone_type in ipairs(sorted) do + print(' ' .. getStoneDescription(opts, stone_type)) + end +else + selectStoneTile(opts, positionals[1]:lower()) +end From 81712dc63afcf2a562e1b972633ec0531ff3ae2e Mon Sep 17 00:00:00 2001 From: Omar El-Koussy Date: Sun, 14 Jun 2026 22:25:47 -0400 Subject: [PATCH 2/5] Revert "Add locate-stone tool and docs" This reverts commit 519e92d53bc8eb4ccd2bdf82d3a0839281c3a785. --- docs/locate-stone.rst | 64 ------------ locate-stone.lua | 233 ------------------------------------------ 2 files changed, 297 deletions(-) delete mode 100644 docs/locate-stone.rst delete mode 100644 locate-stone.lua diff --git a/docs/locate-stone.rst b/docs/locate-stone.rst deleted file mode 100644 index 7bc5b3135..000000000 --- a/docs/locate-stone.rst +++ /dev/null @@ -1,64 +0,0 @@ -locate-stone -============ - -.. dfhack-tool:: - :summary: Finds stone and ore mineral events on the map. - :tags: fort armok inspection productivity - -Fork of `locate-ore` to also include stone and fuel sources. -This tool scans the map for stone and ore mineral events. With no arguments, or with -``list``, prints a list of stone and ore types with tile -counts. - -With a stone or ore or metal argument, selects a random matching undesignated wall tile, -centers the camera on it, and designates it for digging. - -If you want to dig **all** tiles of that kind of ore, highlight that tile with the -keyboard cursor and run `digtype `. - -By default, the tool only searches ore veins that your dwarves have discovered. - -Note that looking for a particular metal might find an ore that contains that -metal along with other metals. For example, locating silver may find -tetrahedrite, which contains silver and copper. - -``locate-stone`` only scans mineral events, so it is quick but does not find -ordinary layer-stone walls like flux stones. - -Usage ------ - -``locate-stone`` - List discovered visible stone and ore mineral events. - -``locate-stone [] `` - Select a random matching undesignated wall tile and designate it for digging. - -Examples --------- - -``locate-stone -all`` - List all mineral events on the map, including those that have not been discovered. - -``locate-stone iron`` - Find and designate a visible tile of any ore that produces iron. - -``locate-stone magnetite`` - Find and designate a visible magnetite tile. - -``locate-stone coal`` - Find and designate a visible bituminous coal or lignite tile. - -``locate-stone -a lignite`` - Include undiscovered/unrevealed mineral events when searching for lignite. - -Options -------- - -``-a``, ``--all`` - Include undiscovered/unrevealed mineral events. - -Aliases -------- - -``coal``, ``coke``, and ``fuel`` match both ``coal_bituminous`` and ``lignite``. diff --git a/locate-stone.lua b/locate-stone.lua deleted file mode 100644 index 5582ded83..000000000 --- a/locate-stone.lua +++ /dev/null @@ -1,233 +0,0 @@ --- Scan the map for stone and ore veins - -local argparse = require('argparse') - -local function extractKeys(target_table) - local keyset = {} - for k, _ in pairs(target_table) do - table.insert(keyset, k) - end - return keyset -end - -local function getRandomTableKey(target_table) - if not target_table then - return nil - end - - local keyset = extractKeys(target_table) - if #keyset == 0 then - return nil - end - - return keyset[math.random(#keyset)] -end - -local function getRandomFromTable(target_table) - if not target_table then - return nil - end - - local key = getRandomTableKey(target_table) - if not key then - return nil - end - - return target_table[key] -end - -local function sortTableBy(tbl, sort_func) - local sorted = {} - for _, value in pairs(tbl) do - table.insert(sorted, value) - end - - table.sort(sorted, sort_func) - - return sorted -end - -local function matchesMetalOreById(mat_indices, target_ore) - for _, mat_index in ipairs(mat_indices) do - local metal_raw = df.global.world.raws.inorganics.all[mat_index] - if metal_raw ~= nil and string.lower(metal_raw.id) == target_ore then - return true - end - end - - return false -end - -local product_by_inorganic_id = { - coal_bituminous = {'coke'}, - lignite = {'coke'}, -} - -local alias_targets = { - coal = { - coal_bituminous = true, - lignite = true, - }, - coke = { - coal_bituminous = true, - lignite = true, - }, - fuel = { - coal_bituminous = true, - lignite = true, - }, -} - -local function matchesStoneAlias(raw_id, target_stone) - local targets = alias_targets[target_stone] - return targets ~= nil and targets[raw_id] == true -end - -local function hasFlag(flags, flag) - if not flags then - return false - end - - local ok, value = pcall(function() return flags[flag] end) - return ok and value -end - -local function isStoneOrOre(ino_raw) - return hasFlag(ino_raw.flags, 'METAL_ORE') or - hasFlag(ino_raw.material.flags, 'IS_STONE') -end - -local tile_attrs = df.tiletype.attrs - -local function isValidMineralTile(opts, pos, check_designation) - if not opts.all and not dfhack.maps.isTileVisible(pos) then return false end - local tt = dfhack.maps.getTileType(pos) - if not tt then return false end - return tile_attrs[tt].material == df.tiletype_material.MINERAL and - (not check_designation or dfhack.maps.getTileFlags(pos).dig == df.tile_dig_designation.No) and - tile_attrs[tt].shape == df.tiletype_shape.WALL -end - -local function findStones(opts, check_designation, target_stone) - local stone_types = {} - for _, block in ipairs(df.global.world.map.map_blocks) do - for _, bevent in ipairs(block.block_events) do - if bevent:getType() ~= df.block_square_event_type.mineral then - goto skipevent - end - - local ino_raw = df.global.world.raws.inorganics.all[bevent.inorganic_mat] - if not isStoneOrOre(ino_raw) then - goto skipevent - end - - if not opts.all and not bevent.flags.discovered then - goto skipevent - end - - local lower_raw = string.lower(ino_raw.id) - local matches_target = not target_stone or lower_raw == target_stone or matchesStoneAlias(lower_raw, target_stone) - if not matches_target and hasFlag(ino_raw.flags, 'METAL_ORE') and ino_raw.metal_ore then - matches_target = matchesMetalOreById(ino_raw.metal_ore.mat_index, target_stone) - end - - if matches_target then - local positions = ensure_key(stone_types, bevent.inorganic_mat, { - inorganic_id = ino_raw.id, - inorganic_mat = bevent.inorganic_mat, - metal_ore = ino_raw.metal_ore, - positions = {} - }).positions - local block_pos = block.map_pos - for y=0,15 do - local row = bevent.tile_bitmask.bits[y] - for x=0,15 do - if row & (1 << x) ~= 0 then - local pos = xyz2pos(block_pos.x + x, block_pos.y + y, block_pos.z) - if isValidMineralTile(opts, pos, check_designation) then - table.insert(positions, pos) - end - end - end - end - end - :: skipevent :: - end - end - - -- trim veins with zero valid tiles - for key,vein in pairs(stone_types) do - if #vein.positions == 0 then - stone_types[key] = nil - end - end - - return stone_types -end - -local function designateDig(pos) - local designation = dfhack.maps.getTileFlags(pos) - designation.dig = df.tile_dig_designation.Default - dfhack.maps.getTileBlock(pos).flags.designated = true -end - -local function getStoneDescription(opts, vein) - local visible = opts.all and '' or 'visible ' - local str = ('%5d %stile(s) of %s'):format(#vein.positions, visible, tostring(vein.inorganic_id):lower()) - if vein.metal_ore and #vein.metal_ore.mat_index > 0 then - str = str .. ' (' - for _, mat_index in ipairs(vein.metal_ore.mat_index) do - local metal_raw = df.global.world.raws.inorganics.all[mat_index] - str = ('%s%s, '):format(str, string.lower(metal_raw.id)) - end - str = str:gsub(', %s*$', '') .. ')' - elseif product_by_inorganic_id[string.lower(tostring(vein.inorganic_id))] then - str = str .. ' (' - for _, product_id in ipairs(product_by_inorganic_id[string.lower(tostring(vein.inorganic_id))]) do - str = ('%s%s, '):format(str, product_id) - end - str = str:gsub(', %s*$', '') .. ')' - end - - return str -end - -local function selectStoneTile(opts, target_stone) - local stone_types = findStones(opts, true, target_stone) - local target_vein = getRandomFromTable(stone_types) - if target_vein == nil then - local visible = opts.all and '' or 'visible ' - qerror('Cannot find any undesignated ' .. visible .. target_stone) - end - local target_pos = target_vein.positions[math.random(#target_vein.positions)] - dfhack.gui.revealInDwarfmodeMap(target_pos, true, true) - designateDig(target_pos) - print(('Here is some %s'):format(target_vein.inorganic_id)) -end - -local opts = { - all=false, - help=false, -} - -local positionals = argparse.processArgsGetopt({...}, { - {'a', 'all', handler=function() opts.all = true end}, - {'h', 'help', handler=function() opts.help = true end}, -}) - -local target_stone = positionals[1] -if target_stone == 'help' or opts.help then - print(dfhack.script_help()) - return -end - -if not target_stone or target_stone == 'list' then - local stone_types = findStones(opts, false) - local sorted = sortTableBy(stone_types, function(a, b) return #a.positions < #b.positions end) - - for _,stone_type in ipairs(sorted) do - print(' ' .. getStoneDescription(opts, stone_type)) - end -else - selectStoneTile(opts, positionals[1]:lower()) -end From 0f6cb39ed415c949589a480cfb1696ac78e1b962 Mon Sep 17 00:00:00 2001 From: Omar El-Koussy Date: Sun, 14 Jun 2026 22:34:41 -0400 Subject: [PATCH 3/5] Add locate-stone script and docs Linked with issue 2679. Add locate-stone.lua and accompanying docs/locate-stone.rst. The new DFHack script (a fork of locate-ore) scans mineral events for stone, ore and fuel sources. Linked with issue 2679. Fork of locate-ore that also shows stones and bituminous coal and lignite. Flux stones are not mineral events and so was not able to add them. --- docs/locate-stone.rst | 64 ++++++++++++ locate-stone.lua | 233 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 docs/locate-stone.rst create mode 100644 locate-stone.lua diff --git a/docs/locate-stone.rst b/docs/locate-stone.rst new file mode 100644 index 000000000..7bc5b3135 --- /dev/null +++ b/docs/locate-stone.rst @@ -0,0 +1,64 @@ +locate-stone +============ + +.. dfhack-tool:: + :summary: Finds stone and ore mineral events on the map. + :tags: fort armok inspection productivity + +Fork of `locate-ore` to also include stone and fuel sources. +This tool scans the map for stone and ore mineral events. With no arguments, or with +``list``, prints a list of stone and ore types with tile +counts. + +With a stone or ore or metal argument, selects a random matching undesignated wall tile, +centers the camera on it, and designates it for digging. + +If you want to dig **all** tiles of that kind of ore, highlight that tile with the +keyboard cursor and run `digtype `. + +By default, the tool only searches ore veins that your dwarves have discovered. + +Note that looking for a particular metal might find an ore that contains that +metal along with other metals. For example, locating silver may find +tetrahedrite, which contains silver and copper. + +``locate-stone`` only scans mineral events, so it is quick but does not find +ordinary layer-stone walls like flux stones. + +Usage +----- + +``locate-stone`` + List discovered visible stone and ore mineral events. + +``locate-stone [] `` + Select a random matching undesignated wall tile and designate it for digging. + +Examples +-------- + +``locate-stone -all`` + List all mineral events on the map, including those that have not been discovered. + +``locate-stone iron`` + Find and designate a visible tile of any ore that produces iron. + +``locate-stone magnetite`` + Find and designate a visible magnetite tile. + +``locate-stone coal`` + Find and designate a visible bituminous coal or lignite tile. + +``locate-stone -a lignite`` + Include undiscovered/unrevealed mineral events when searching for lignite. + +Options +------- + +``-a``, ``--all`` + Include undiscovered/unrevealed mineral events. + +Aliases +------- + +``coal``, ``coke``, and ``fuel`` match both ``coal_bituminous`` and ``lignite``. diff --git a/locate-stone.lua b/locate-stone.lua new file mode 100644 index 000000000..5582ded83 --- /dev/null +++ b/locate-stone.lua @@ -0,0 +1,233 @@ +-- Scan the map for stone and ore veins + +local argparse = require('argparse') + +local function extractKeys(target_table) + local keyset = {} + for k, _ in pairs(target_table) do + table.insert(keyset, k) + end + return keyset +end + +local function getRandomTableKey(target_table) + if not target_table then + return nil + end + + local keyset = extractKeys(target_table) + if #keyset == 0 then + return nil + end + + return keyset[math.random(#keyset)] +end + +local function getRandomFromTable(target_table) + if not target_table then + return nil + end + + local key = getRandomTableKey(target_table) + if not key then + return nil + end + + return target_table[key] +end + +local function sortTableBy(tbl, sort_func) + local sorted = {} + for _, value in pairs(tbl) do + table.insert(sorted, value) + end + + table.sort(sorted, sort_func) + + return sorted +end + +local function matchesMetalOreById(mat_indices, target_ore) + for _, mat_index in ipairs(mat_indices) do + local metal_raw = df.global.world.raws.inorganics.all[mat_index] + if metal_raw ~= nil and string.lower(metal_raw.id) == target_ore then + return true + end + end + + return false +end + +local product_by_inorganic_id = { + coal_bituminous = {'coke'}, + lignite = {'coke'}, +} + +local alias_targets = { + coal = { + coal_bituminous = true, + lignite = true, + }, + coke = { + coal_bituminous = true, + lignite = true, + }, + fuel = { + coal_bituminous = true, + lignite = true, + }, +} + +local function matchesStoneAlias(raw_id, target_stone) + local targets = alias_targets[target_stone] + return targets ~= nil and targets[raw_id] == true +end + +local function hasFlag(flags, flag) + if not flags then + return false + end + + local ok, value = pcall(function() return flags[flag] end) + return ok and value +end + +local function isStoneOrOre(ino_raw) + return hasFlag(ino_raw.flags, 'METAL_ORE') or + hasFlag(ino_raw.material.flags, 'IS_STONE') +end + +local tile_attrs = df.tiletype.attrs + +local function isValidMineralTile(opts, pos, check_designation) + if not opts.all and not dfhack.maps.isTileVisible(pos) then return false end + local tt = dfhack.maps.getTileType(pos) + if not tt then return false end + return tile_attrs[tt].material == df.tiletype_material.MINERAL and + (not check_designation or dfhack.maps.getTileFlags(pos).dig == df.tile_dig_designation.No) and + tile_attrs[tt].shape == df.tiletype_shape.WALL +end + +local function findStones(opts, check_designation, target_stone) + local stone_types = {} + for _, block in ipairs(df.global.world.map.map_blocks) do + for _, bevent in ipairs(block.block_events) do + if bevent:getType() ~= df.block_square_event_type.mineral then + goto skipevent + end + + local ino_raw = df.global.world.raws.inorganics.all[bevent.inorganic_mat] + if not isStoneOrOre(ino_raw) then + goto skipevent + end + + if not opts.all and not bevent.flags.discovered then + goto skipevent + end + + local lower_raw = string.lower(ino_raw.id) + local matches_target = not target_stone or lower_raw == target_stone or matchesStoneAlias(lower_raw, target_stone) + if not matches_target and hasFlag(ino_raw.flags, 'METAL_ORE') and ino_raw.metal_ore then + matches_target = matchesMetalOreById(ino_raw.metal_ore.mat_index, target_stone) + end + + if matches_target then + local positions = ensure_key(stone_types, bevent.inorganic_mat, { + inorganic_id = ino_raw.id, + inorganic_mat = bevent.inorganic_mat, + metal_ore = ino_raw.metal_ore, + positions = {} + }).positions + local block_pos = block.map_pos + for y=0,15 do + local row = bevent.tile_bitmask.bits[y] + for x=0,15 do + if row & (1 << x) ~= 0 then + local pos = xyz2pos(block_pos.x + x, block_pos.y + y, block_pos.z) + if isValidMineralTile(opts, pos, check_designation) then + table.insert(positions, pos) + end + end + end + end + end + :: skipevent :: + end + end + + -- trim veins with zero valid tiles + for key,vein in pairs(stone_types) do + if #vein.positions == 0 then + stone_types[key] = nil + end + end + + return stone_types +end + +local function designateDig(pos) + local designation = dfhack.maps.getTileFlags(pos) + designation.dig = df.tile_dig_designation.Default + dfhack.maps.getTileBlock(pos).flags.designated = true +end + +local function getStoneDescription(opts, vein) + local visible = opts.all and '' or 'visible ' + local str = ('%5d %stile(s) of %s'):format(#vein.positions, visible, tostring(vein.inorganic_id):lower()) + if vein.metal_ore and #vein.metal_ore.mat_index > 0 then + str = str .. ' (' + for _, mat_index in ipairs(vein.metal_ore.mat_index) do + local metal_raw = df.global.world.raws.inorganics.all[mat_index] + str = ('%s%s, '):format(str, string.lower(metal_raw.id)) + end + str = str:gsub(', %s*$', '') .. ')' + elseif product_by_inorganic_id[string.lower(tostring(vein.inorganic_id))] then + str = str .. ' (' + for _, product_id in ipairs(product_by_inorganic_id[string.lower(tostring(vein.inorganic_id))]) do + str = ('%s%s, '):format(str, product_id) + end + str = str:gsub(', %s*$', '') .. ')' + end + + return str +end + +local function selectStoneTile(opts, target_stone) + local stone_types = findStones(opts, true, target_stone) + local target_vein = getRandomFromTable(stone_types) + if target_vein == nil then + local visible = opts.all and '' or 'visible ' + qerror('Cannot find any undesignated ' .. visible .. target_stone) + end + local target_pos = target_vein.positions[math.random(#target_vein.positions)] + dfhack.gui.revealInDwarfmodeMap(target_pos, true, true) + designateDig(target_pos) + print(('Here is some %s'):format(target_vein.inorganic_id)) +end + +local opts = { + all=false, + help=false, +} + +local positionals = argparse.processArgsGetopt({...}, { + {'a', 'all', handler=function() opts.all = true end}, + {'h', 'help', handler=function() opts.help = true end}, +}) + +local target_stone = positionals[1] +if target_stone == 'help' or opts.help then + print(dfhack.script_help()) + return +end + +if not target_stone or target_stone == 'list' then + local stone_types = findStones(opts, false) + local sorted = sortTableBy(stone_types, function(a, b) return #a.positions < #b.positions end) + + for _,stone_type in ipairs(sorted) do + print(' ' .. getStoneDescription(opts, stone_type)) + end +else + selectStoneTile(opts, positionals[1]:lower()) +end From 5d2a204abf52a9250f7cdbcd3ff5b8a9b6cbb680 Mon Sep 17 00:00:00 2001 From: Omar El-Koussy Date: Sun, 14 Jun 2026 22:45:09 -0400 Subject: [PATCH 4/5] Add locate-stone entry to changelog --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 33844aaaa..9494f197e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,8 @@ Template for new versions: ## New Tools +- 'locate-stone': fork of 'locate-ore' but also includes stone and fuel sources + ## New Features ## Fixes From 81745ab162f4368df7e8126a430751ffe4f8021a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 02:56:09 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/locate-stone.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/locate-stone.rst b/docs/locate-stone.rst index 7bc5b3135..5fd58e8c4 100644 --- a/docs/locate-stone.rst +++ b/docs/locate-stone.rst @@ -3,14 +3,14 @@ locate-stone .. dfhack-tool:: :summary: Finds stone and ore mineral events on the map. - :tags: fort armok inspection productivity + :tags: fort armok inspection productivity Fork of `locate-ore` to also include stone and fuel sources. This tool scans the map for stone and ore mineral events. With no arguments, or with ``list``, prints a list of stone and ore types with tile -counts. +counts. -With a stone or ore or metal argument, selects a random matching undesignated wall tile, +With a stone or ore or metal argument, selects a random matching undesignated wall tile, centers the camera on it, and designates it for digging. If you want to dig **all** tiles of that kind of ore, highlight that tile with the