Skip to content
Merged
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
2 changes: 2 additions & 0 deletions advtools.lua
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
--@ module=true

local convo = reqscript('internal/advtools/convo')
local fastcombat = reqscript('internal/advtools/fastcombat')
local party = reqscript('internal/advtools/party')

OVERLAY_WIDGETS = {
conversation=convo.AdvRumorsOverlay,
fastcombat=fastcombat.AdvCombatOverlay,
}

if dfhack_flags.module then
Expand Down
187 changes: 187 additions & 0 deletions autocheese.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
--@module = true

local ic = reqscript('idle-crafting')

---make cheese using a specific barrel and workshop
---@param barrel df.item
---@param workshop df.building_workshopst
---@return df.job
function makeCheese(barrel, workshop)
---@type df.job
local job = ic.make_job()
job.job_type = df.job_type.MakeCheese

local jitem = df.job_item:new()
jitem.quantity = 0
jitem.vector_id = df.job_item_vector_id.ANY_COOKABLE
jitem.flags1.unrotten = true
jitem.flags1.milk = true
job.job_items.elements:insert('#', jitem)

if not dfhack.job.attachJobItem(job, barrel, df.job_item_ref.T_role.Reagent, 0, -1) then
dfhack.error('could not attach item')
end

ic.assignToWorkshop(job, workshop)
return job
end



---unit is ready to take jobs
---@param unit df.unit
---@return boolean
function unitIsAvailable(unit)
if unit.job.current_job then
return false
elseif #unit.individual_drills > 0 then
return false
elseif unit.flags1.caged or unit.flags1.chained then
return false
elseif unit.military.squad_id ~= -1 then
local squad = df.squad.find(unit.military.squad_id)
-- this lookup should never fail
---@diagnostic disable-next-line: need-check-nil
return #squad.orders == 0 and squad.activity == -1
end
return true
end

---check if unit can perform labor at workshop
---@param unit df.unit
---@param unit_labor df.unit_labor
---@param workshop df.building
---@return boolean
function availableLaborer(unit, unit_labor, workshop)
return unit.status.labors[unit_labor]
and unitIsAvailable(unit)
and ic.canAccessWorkshop(unit, workshop)
end

---find unit with a particular labor enabled
---@param unit_labor df.unit_labor
---@param job_skill df.job_skill
---@param workshop df.building
---@return df.unit|nil
---@return integer|nil
function findAvailableLaborer(unit_labor, job_skill, workshop)
local max_unit = nil
local max_skill = -1
for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do
if
availableLaborer(unit, unit_labor, workshop)
then
local unit_skill = dfhack.units.getNominalSkill(unit, job_skill, true)
if unit_skill > max_skill then
max_unit = unit
max_skill = unit_skill
end
end
end
return max_unit, max_skill
end

local function findMilkBarrel(min_liquids)
for _, container in ipairs(df.global.world.items.other.FOOD_STORAGE) do
if
not (container.flags.in_job or container.flags.forbid) and
container.flags.container and #container.general_refs >= min_liquids
then
local content_reference = dfhack.items.getGeneralRef(container, df.general_ref_type.CONTAINS_ITEM)
local contained_item = df.item.find(content_reference and content_reference.item_id or -1)
if contained_item then
local mat_info = dfhack.matinfo.decode(contained_item)
if mat_info:matches { milk = true } then
return container
end
end
end
end
end

---find a workshop to which the barrel can be brought
---if the workshop has a master, only return workshop and master if the master is available
---@param pos df.coord
---@return df.building_workshopst?
---@return df.unit?
function findWorkshop(pos)
for _,workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do
if
dfhack.maps.canWalkBetween(pos, xyz2pos(workshop.centerx, workshop.centery, workshop.z)) and
not workshop.profile.blocked_labors[df.unit_labor.MAKE_CHEESE] and
#workshop.jobs == 0
then
if #workshop.profile.permitted_workers == 0 then
-- immediately return workshop without master
return workshop, nil
else
unit = df.unit.find(workshop.profile.permitted_workers[0])
if
unit and availableLaborer(unit, df.unit_labor.MAKE_CHEESE, workshop)
then
-- return workshop and master, if master is available
return workshop, unit
else
print("autocheese: Skipping farmer's workshop with unavailable master")
end
end
end
end
end

if dfhack_flags.module then
return
end

-- actual script action

local argparse = require('argparse')

local min_number = 50

local _ = argparse.processArgsGetopt({...},
{
{ 'm', 'min-milk', hasArg = true,
handler = function(min)
min_number = argparse.nonnegativeInt(min, 'min-milk')
end }
})


local reagent = findMilkBarrel(min_number)

if not reagent then
-- print('autocheese: no sufficiently full barrel found')
return
end

local workshop, worker = findWorkshop(xyz2pos(dfhack.items.getPosition(reagent)))

if not workshop then
print("autocheese: no Farmer's Workshop available")
return
end

-- try to find laborer for workshop without master
if not worker then
worker, _ = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop)
end

if not worker then
print('autocheese: no cheesemaker available')
return
end
local job = makeCheese(reagent, workshop)

print(('autocheese: dispatching cheesemaking job for %s (%d milk) to %s'):format(
dfhack.df2console(dfhack.items.getReadableDescription(reagent)),
#reagent.general_refs,
dfhack.df2console(dfhack.units.getReadableName(worker))
))


-- assign a worker and send it to fetch the barrel
dfhack.job.addWorker(job, worker)
dfhack.units.setPathGoal(worker, reagent.pos, df.unit_path_goal.GrabJobResources)
job.items[0].flags.is_fetching = true
job.flags.fetching = true
4 changes: 4 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ Template for new versions:
# Future

## New Tools
- `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk

## New Features
- `advtools`: new overlay ``advtools.fastcombat``; allows you to skip combat animations and the announcement "More" button

## Fixes

## Misc Improvements
- `hide-tutorials`: if enabled, also hide tutorial popups for adventure mode
- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game

## Removed

Expand Down
13 changes: 13 additions & 0 deletions docs/advtools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,16 @@ enemies will gain the ``slay`` and ``kill`` keywords. It will also add
additional conversation options for asking whereabouts of your relationships --
in vanilla, you can only ask whereabouts of historical figures involved in
rumors you personally witnessed or heard about.

``advtools.fastcombat``
~~~~~~~~~~~~~~~~~~~~~~~

When enabled, this overlay will allow you to skip most combat animations,
including the whooshes and projectiles travelling through the screen. It will
also let you skip the announcements window when the "More" button is active,
scrolling you to the very bottom with the first press, and skipping the window
entirely with the second press. This drastically speeds up combat while still
giving you the option not to skip the announcements. Skip keys are left mouse click,
the SELECT button, the movement keys and combat-related keys that don't bring up a
menu (such as bump attack). If clicking to skip past combat, it will only skip the
announcements if you're clicking outside the announcements panel.
39 changes: 39 additions & 0 deletions docs/autocheese.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
autocheese
==========

.. dfhack-tool::
:summary: Schedule cheese making jobs based on milk reserves.
:tags: fort auto

Cheese making is difficult to automate using work orders. A single job
can consume anything from a bucket with a single unit of milk to a barrel
with 100 units of milk. This makes it hard to predict how much cheese will
actually be produced by an automated order.

The script will scan your fort for barrels with a certain minimum amount of milk
(default: 50), create a cheese making job specifically for that barrel, and
assign this job to one of your idle dwarves (giving preference to skilled cheese
makers).

When enabled using `gui/control-panel`, the script will run automatically, with
default options, twice a month.

Usage
-----

::

autocheese [<options>]

Examples
--------

``autocheese -m 100``
Only create a job if there is a barrel that is filled to the maximum.

Options
-------

``-m``, ``--min-milk``
Set the minimum number of milk items in a barrel for the barrel to be
considered for cheese making.
14 changes: 10 additions & 4 deletions docs/hide-tutorials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ hide-tutorials

.. dfhack-tool::
:summary: Hide new fort tutorial popups.
:tags: fort interface
:tags: adventure fort interface

If you've played the game before and don't need to see the tutorial popups that
show up on every new fort, ``hide-tutorials`` can hide them for you. You can
enable this tool as a system service in the "Services" tab of
`gui/control-panel` so it takes effect for all new or loaded forts.
`gui/control-panel` so it takes effect for all forts and adventures.

Specifically, this tool hides:

- The popup displayed when creating a new world
- The "Do you want to start a tutorial embark" popup
- Popups displayed the first time you open the labor, burrows, justice, and
other similar screens in a new fort
- Popups displayed when you perform certain actions for the first time in an
adventure

Note that only unsolicited tutorial popups are hidden. If you directly request
a tutorial page from the help, then it will still function normally.
Expand All @@ -27,6 +29,10 @@ Usage

enable hide-tutorials
hide-tutorials
hide-tutorials reset

If you haven't enabled the tool, but you run the command while a fort is
loaded, all future popups for the loaded fort will be hidden.
If you haven't enabled the tool, but you run the command while a fort or
adventure is loaded, all future popups for the loaded game will be hidden.

If you run the command with the ``reset`` option, all popups will be re-enabled
as if they had never been seen or dismissed.
37 changes: 28 additions & 9 deletions hide-tutorials.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ function isEnabled()
return enabled
end

local function is_fort_map_loaded()
return df.global.gamemode == df.game_mode.DWARF and dfhack.isMapLoaded()
end

local help = df.global.game.main_interface.help

local function close_help()
Expand Down Expand Up @@ -43,15 +39,36 @@ function skip_tutorial_prompt()
end
end

local function get_prefix()
if dfhack.world.isFortressMode() then
return 'POPUP_'
elseif dfhack.world.isAdventureMode() then
return 'ADVENTURE_POPUP_'
end
end

local function hide_all_popups()
local prefix = get_prefix()
if not prefix then return end
for i,name in ipairs(df.help_context_type) do
if not name:startswith('POPUP_') then goto continue end
if not name:startswith(prefix) then goto continue end
utils.insert_sorted(df.global.plotinfo.tutorial_seen, i)
utils.insert_sorted(df.global.plotinfo.tutorial_hide, i)
::continue::
end
end

local function show_all_popups()
local prefix = get_prefix()
if not prefix then return end
for i,name in ipairs(df.help_context_type) do
if not name:startswith(prefix) then goto continue end
utils.erase_sorted(df.global.plotinfo.tutorial_seen, i)
utils.erase_sorted(df.global.plotinfo.tutorial_hide, i)
::continue::
end
end

dfhack.onStateChange[GLOBAL_KEY] = function(sc)
if not enabled then return end

Expand All @@ -65,7 +82,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc)
dfhack.timeout(100, 'frames', skip_tutorial_prompt)
dfhack.timeout(1000, 'frames', skip_tutorial_prompt)
end
elseif sc == SC_MAP_LOADED and df.global.gamemode == df.game_mode.DWARF then
elseif sc == SC_MAP_LOADED then
hide_all_popups()
end
end
Expand All @@ -81,13 +98,15 @@ end

if args[1] == "enable" then
enabled = true
if is_fort_map_loaded() then
if dfhack.isMapLoaded() then
hide_all_popups()
end
elseif args[1] == "disable" then
enabled = false
elseif is_fort_map_loaded() then
elseif args[1] == "reset" then
show_all_popups()
elseif dfhack.isMapLoaded() then
hide_all_popups()
else
qerror('hide-tutorials needs a loaded fortress map to work')
qerror('hide-tutorials needs a loaded fortress or adventure map to work')
end
2 changes: 2 additions & 0 deletions internal/control-panel/registry.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ COMMANDS_BY_IDX = {
{command='autobutcher target 10 10 14 2 BIRD_PEAFOWL_BLUE', group='automation', mode='run',
desc='Enable if you usually want to raise peafowl.'},
{command='autochop', group='automation', mode='enable'},
{command='autocheese', group='automation', mode='repeat',
params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'autocheese', ']'}},
{command='autoclothing', group='automation', mode='enable'},
{command='autofarm', group='automation', mode='enable'},
{command='autofarm threshold 150 grass_tail_pig', group='automation', mode='run',
Expand Down