Skip to content

Add zprospectanalyzer #1465

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/zprospectanalyzer.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
zprospectanalyzer
=================

**Goal**: Filter and print stones that are worth **3 points**.

Features
--------

- **Output Parsing**: Automatically runs ``prospect all`` and parses the text output.
- **Section Filtering**: Optionally filters materials by specific sections like ores or gems.
- **Presets**: Running ``zprospectanalyzer`` **without parameters** will only run the preset of **3-point stones**.
- **Missing Materials Reporting**: Displays ``<not found>`` next to any requested material that doesn't appear in the output.

Usage
-----

.. code-block:: bash

zprospectanalyzer [material1] [material2] ...

Example
-------

.. code-block:: bash

zprospectanalyzer claystone granite ruby tetrahedrite
163 changes: 163 additions & 0 deletions zprospectanalyzer.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
-- scripts/zprospectanalyzer.lua

local zprospectanalyzer = {}

--- Scans the world using the `prospect` command and returns a table of materials.
-- @return table data[section][material_key] = { count=number, minElev=number?, maxElev=number? }
function zprospectanalyzer.scanProspect(section)
local filterSection = section and section:lower():gsub("%s+","_") or "all"
local showMap = {
base_materials = "base",
liquids = "liquids",
layer_materials = "layers",
features = "features",
ores = "ores",
gems = "gems",
other_vein_stone = "veins",
shrubs = "shrubs",
wood_in_trees = "trees",
}
local cmd = { "prospect", "all" }
if filterSection ~= "all" and showMap[filterSection] then
table.insert(cmd, "--show")
table.insert(cmd, showMap[filterSection])
end
local output, status = dfhack.run_command_silent(table.unpack(cmd))
if status ~= CR_OK then
error(("prospect failed (code %d): %s"):format(status, tostring(output)))
end
local data = {}
local current
-- split output into lines without interpreting escape sequences
for line in output:gmatch("([^\n]+)") do
local header = line:match("^%s*([%a%s_]+)%s*:%s*$")
if header then
current = header:lower():gsub("%s+","_")
data[current] = {}
elseif current then
local name, count, elev = line:match(
"^%s*([%u_]+)%s*:%s*(%d+)%s+Elev:?%s*([-%d%.]+)"
)
if not name then
name, count = line:match("^%s*([%u_]+)%s*:%s*(%d+)")
end
if name and count then
local key = name:lower()
local entry = { count = tonumber(count) }
if elev then
local minE, maxE = elev:match("([-%d]+)%.%.([-%d]+)")
entry.minElev = tonumber(minE)
entry.maxElev = tonumber(maxE)
end
data[current][key] = entry
end
end
end
return data
end

--- Sorts an array of material entries by min elevation then quantity.
-- @return sorted list
function zprospectanalyzer.sortMaterials(list)
table.sort(list, function(a, b)
local am = a.entry.minElev or 0
local bm = b.entry.minElev or 0
if am ~= bm then
return am > bm
end
return a.entry.count > b.entry.count
end)
return list
end

--- Prints material entries in aligned columns with a header.
function zprospectanalyzer.printMaterials(list)
-- Header line
print(string.format(" %-15s %8s %10s %10s", "Material", "Quantity", "Min Elev", "Max Elev"))
for _, item in ipairs(list) do
print(string.format(
" %-15s %8d %10s %10s",
item.key:upper(),
item.entry.count,
item.entry.minElev or "?",
item.entry.maxElev or "?"
))
end
end

--- Main entry point for CLI.
-- Supports "blocks" preset or custom section/material arguments.
-- Not-found entries are printed last.
-- blocks preset includes stones that are worth 3 pts.
function zprospectanalyzer.main(...)
local args = { ... }
if #args == 0 then args = { "blocks" } end
local presets = {
blocks = {
"Alabaster", "Alunite", "Andesite", "Anhydrite", "Basalt",
Copy link
Member

@quietust quietust Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be hardcoding - it should fetch the actual stone IDs from the current world's inorganic material definitions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized I didn’t put the initial purpose of the program. The goal is to output stone that is worth 3 points in the embark shop menu.

I’ll review the program today. I can clean it up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no guarantee even that any of those stones exist in any given world, or if they do that they're worth 3 points in the embark menu. For that you need to pull the material definitions out of the world

DFHack tools should, to the extent practical, work even with radically modified raws

Copy link
Author

@unboundlopez unboundlopez Jun 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yesterday I tried looking for the skill point value of items and failed. I don’t like the way I’m doing it now but can’t seem to find way to achieve a preset of blocks worth 3 points. I’ll try searching again today.

"Bauxite", "Bismuthinite", "Borax", "Brimstone", "Chert",
"Chromite", "Cinnabar", "Claystone", "Cobaltite", "Conglomerate",
"Cryolite", "Dacite", "Diorite", "Gabbro", "Gneiss",
"Granite", "Graphite", "Gypsum", "Hornblende", "Ilmenite",
"Jet", "Kaolinite", "Kimberlite", "Marcasite", "Mica",
"Microcline", "Olivine", "Orpiment", "Orthoclase", "Periclase",
"Petrified_wood", "Phyllite", "Pitchblende", "Puddingstone",
"Pyrolusite", "Quartzite", "Realgar", "Rhyolite", "Rock_salt",
"Rutile", "Saltpeter", "Sandstone", "Satinspar", "Schist",
"Selenite", "Serpentine", "Shale", "Siltstone", "Slate",
"Stibnite", "Sylvite", "Talc",
}
}
local first = args[1]:lower():gsub("%s+","_")
local materials = {}
local section
if presets[first] then
materials = presets[first]
else
local validSections = {
base_materials=true, liquids=true, layer_materials=true,
features=true, ores=true, gems=true,
other_vein_stone=true, shrubs=true, wood_in_trees=true
}
local startIndex = 1
if validSections[first] then section = first; startIndex = 2 end
for i = startIndex, #args do materials[#materials+1] = args[i] end
end
local data = zprospectanalyzer.scanProspect(section)
local foundEntries = {}
local missingEntries = {}
for _, mat in ipairs(materials) do
local key = mat:lower():gsub("%s+","_")
local entryFound = false
if section then
local e = (data[section] or {})[key]
if e then
foundEntries[#foundEntries+1] = { key = key, entry = e }
entryFound = true
end
else
for _, items in pairs(data) do
local e = items[key]
if e then
foundEntries[#foundEntries+1] = { key = key, entry = e }
entryFound = true
end
end
end
if not entryFound then
missingEntries[#missingEntries+1] = mat
end
end
zprospectanalyzer.printMaterials(zprospectanalyzer.sortMaterials(foundEntries))
for _, mat in ipairs(missingEntries) do
print(string.format(
" %-15s : %7s <not found>",
mat:upper(), "-"
))
end
end

-- Execute main when run as script
zprospectanalyzer.main(...)

return zprospectanalyzer